深度讲解 AI Function Calling 和 Model Context Protocol (MCP) 如何构成现代 agent 系统的基石,附完整实现。
在打造一个专注于 AI 函数调用的 AI 智能体框架的过程中,我们深入了解了函数调用。本文将介绍我们在框架开发期间对函数调用的全部探索成果。
Github 仓库:https://github.com/wrtnlabs/agentica
主页:https://wrtnlabs.io/agentica
本文的核心内容:
2023 年 OpenAI 宣布推出 AI 函数调用时,许多人预测它将征服世界。然而,函数调用并未兑现这一承诺,反而被智能体工作流方案掩盖了光芒。
我们找到了原因:对 JSON Schema 缺乏理解,以及编译器层面的支持不足。智能体框架应该强化计算机科学的基本原理,而不是专注于花哨的技术。
OpenAPI 的历史比 MCP 悠久得多,采用范围也广得多。只要库开发者充分理解 JSON Schema 并具备扎实的编译器技术,OpenAPI 就能实现与 MCP 相同的目标。
通过采用我们的编排策略,函数调用能够在提升性能的同时降低 token 成本,即使使用 gpt-4o-mini(8b)这样的小模型也是如此。
让我们携手 Agentica,让函数调用再次伟大。
2023 年,OpenAI 宣布推出 AI Function Calling。
当时,许多 AI 研究人员预测它将彻底改变整个行业。即使在我自己的社交圈里,也有大量开发者争相构建以函数调用为核心的新型 AI 应用。
应用市场将围绕 AI 聊天机器人重新洗牌。
开发者只需要创建或收集 API 函数,AI 就会自动调用它们。人们将不再需要创建复杂的前端或客户端应用程序。
通过聊天完成一切。
但如今情况如何?函数调用征服世界了吗?并没有——在当前的 AI 开发生态中,占据主导地位的不是函数调用,而是智能体工作流驱动的开发方式。langchain 或许是这一工作流生态中最具代表性的框架。
目前,许多 AI 开发者声称,AI 函数调用实现起来很困难,会消耗过多 token,并且经常产生幻觉。他们认为,要让函数调用得到广泛采用,AI 模型必须变得便宜得多、规模更大,也更加智能。
然而,我们有不同的看法。如今的 LLM 已经足够强大,就连规模较小的 8b 模型也是如此,而且这些模型可以在个人笔记本电脑上运行。函数调用未能得到广泛采用,是因为 AI 框架开发者没能准确且有效地构建函数 Schema。
他们没有充分关注计算机科学等基础知识,反而过度依赖华而不实的技术。
函数调用使 AI 模型能够与外部系统交互:模型可以判断何时应该调用函数,并生成调用该函数所需的适当 JSON。
模型不再只是生成自由形式的文本,而是会根据用户请求识别何时需要调用特定函数,并为这些函数调用提供结构化数据。
Model Context Protocol 是一种开放标准,旨在促进 AI 工具与外部数据源之间安全的双向连接。它的目标是简化 AI 助手与各种工具和数据的集成,从而实现更加动态的交互。
MCP 对聊天机器人等 AI 应用与外部系统的交互方式进行了标准化,使其能够更有效地访问和利用数据。
它支持灵活且可扩展的集成,让开发者能够更轻松地构建适配不同数据源和工具的应用程序。
智能体工作流是一种结构化的 AI 智能体运行方式,其中多个专门化的 AI 智能体协同工作,以完成复杂任务。工作流中的每个智能体都具有特定的职责和专业能力,从而形成一条处理流水线,分别处理大型任务的不同方面。
不过,智能体工作流主要是为特定用途的应用而设计的,并非用于创建通用智能体。智能体工作流擅长解决边界清晰、定义明确的问题,这些问题可以被分解为需要不同专业能力的具体步骤。它们通常并不是为了创建一个“无所不能”的单一智能体。
工作流驱动的开发是当前 AI 开发领域的趋势,但这种方法无法实现智能体式 AI。它是函数调用未能达到预期效果后出现的一种替代方案。
{
name: "divide",
arguments: {
x: {
type: "number",
},
y: {
anyOf: [ // not supported in Gemini
{
type: "number",
exclusiveMaximum: 0, // not supported in OpenAI and Gemini
},
{
type: "number",
exclusiveMinimum: 0, // not supported in OpenAI and Gemini
},
],
},
},
description: "Calculate x / y",
}
你可能在 AI 社区中看到过这样的反馈:
我创建了一个可以在 OpenAI GPT 上运行的函数 Schema,但它在 Google Gemini 上无法工作。另外,我的一些函数 Schema 可以在 Claude 上正常运行,但 OpenAI 却抛出异常,提示这是无效的 Schema。
事实上,不同 AI 厂商采用的 JSON Schema 规范并不相同,而且其中一些厂商并未完整支持标准 JSON Schema 规范。例如,OpenAI 不支持 minimum 和 format: uuid 之类的约束属性,而 Gemini 甚至不支持 $ref 和 anyOf 等类型。
具体来说,OpenAI 和 Gemini 都拥有各自定制的 JSON Schema 规范,与标准规范存在差异;Claude 则完整支持 JSON Schema 2020-12 草案。DeepSeek 和 Llama 等其他模型不会限制 JSON Schema 规范,因此可以使用标准 JSON Schema 规范。
准确理解 JSON Schema 规范,是从函数调用迈向智能体式 AI 的第一步。下面列出了各家 AI 厂商的 JSON Schema 规范:
@samchon/openapi IChatGptSchema.ts:OpenAI ChatGPT IClaudeSchema.ts:Anthropic Claude IDeepSeekSchema.ts:High-Flyer DeepSeek IGeminiSchema.ts:Google Gemini ILlamaSchema.ts:Meta Llama
IChatGptSchema.ts:OpenAI ChatGPT
IClaudeSchema.ts:Anthropic Claude
IDeepSeekSchema.ts:High-Flyer DeepSeek
IGeminiSchema.ts:Google Gemini
ILlamaSchema.ts:Meta Llama
import { HttpLlm, IHttpLlmApplication } from "@samchon/openapi";
const app: IHttpLlmApplication<"chatgpt"> = HttpLlm.application({
model: "chatgpt",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((r) => r.json()),
});
console.log(app);
将 OpenAPI 文档转换为 AI 函数调用 Schema
OpenAPI 文档为 API 函数提供了结构良好的规范,并且自 2010 年以来一直保持标准化和稳定。然而,它并未在 AI 函数调用生态中得到广泛采用。与此同时,MCP(Model Context Protocol)尽管是一种更新的协议,却已得到广泛采用。
从生态规模来看,OpenAPI 文档的数量远多于 MCP,而且由于长期以来一直被后端开发者接受为标准,其文档质量也更加出色。那么,为什么 OpenAPI 没有像 MCP 一样在 AI 生态中得到广泛采用?我们认为,原因与上一章讨论的相同:准确的 Schema 问题。
如前所述,不同 AI 厂商采用的 JSON Schema 规范各不相同。此外,每个 OpenAPI 版本也都有自己独特的 JSON Schema 规范。在我们看来,这正是 OpenAPI 未能在函数调用生态中得到广泛接受的原因。人们甚至很难充分理解各家 AI 厂商的 JSON Schema 规范,又怎么能指望他们把特定版本的 OpenAPI 文档转换成适用于不同 AI 厂商的 JSON Schema 呢?相比之下,尽管 MCP 可能得到了 AI 生态重要力量 Anthropic Claude 的推动,但它的规范简洁,而且不需要单独的转换过程。这才是根本原因。
我们认为 MCP 是一种结构良好的协议,但同时也认为,没有必要为了函数调用而从 OpenAPI 迁移到 MCP。OpenAPI 能够实现与 MCP 相同的目标,因此我们建议充分利用现有的 OpenAPI 生态。
为了让 OpenAPI 文档支持类似 MCP 的函数调用,我们会先将 Swagger/OpenAPI 文档转换为经过修订的 OpenAPI v3.1 规范,移除含糊和重复的表达。随后,我们使用与函数结构保持一致的迁移 Schema,将其转换为特定 AI 厂商的函数 Schema。
下面是将某个在线商城的 OpenAPI 文档转换为函数 Schema 的演示代码,以及一段展示它作为实际 AI 智能体运行的视频。只需导入现有在线商城服务器中的 swagger.json 文件,就可以通过聊天搜索商品、购买商品、管理配送、使用优惠券或处理退款。
理解、支持并运用 JSON Schema,就是函数调用的全部要义。
import { Agentica, assertHttpLlmApplication } from "@agentica/core";
import OpenAI from "openai";
const agent = new Agentica({
model: "chatgpt",
vendor: {
model: "gpt-4o-mini",
api: new OpenAI({ apiKey: "********" }),
},
controllers: [
{
protocol: "http",
name: "shopping",
application: assertHttpLlmApplication({
model: "chatgpt",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((res) => res.json()),
}),
connection: {
host: "https://shopping-be.wrtn.ai",
headers: {
Authorization: "Bearer ********",
},
},
}
],
});
await agent.convert("I wanna buy a Macbook.");
import { ILlmApplication } from "@samchon/openapi";
import typia, { tags } from "typia";
const app: ILlmApplication<"chatgpt"> =
typia.llm.application<BbsArticleService, "chatgpt">();
console.log(app);
class BbsArticleService {
/**
* Update an article.
*/
update(props: {
/**
* Target article's {@link IBbsArticle.id}.
*/
id: string & tags.Format<"uuid">;
/**
* New content to update.
*/
input: IBbsArticle.IUpdate;
}): void;
}
interface IBbsArticle { ... }
准确理解 JSON Schema 规范,以及它针对不同 AI 厂商的转换逻辑,主要是库或框架开发者需要做到的事情。一个优秀的库应当让用户只需提供一个函数或类,就能调用 AI 函数,而不必了解 JSON Schema 规范中那些复杂的细节。
为了让用户能够方便地使用 AI 函数调用,并充分发挥智能体式 AI 的潜力,函数 Schema 必须由编译器构建。如果无法通过编程方式生成 AI 函数 Schema,那么函数调用必然会被工作流智能体方案掩盖光芒。
例如,在上一节的商城后端演示中,AI 函数 Schema 的代码行数(LOC)是源代码的 5.62 倍。如果要求用户手动编写 AI 函数 Schema,他们就需要投入编写原始源代码所需工作量的 5.62 倍。
源代码开发有编译器和 IDE 辅助,而 JSON Schema 开发缺乏类似的支持,因此更容易出错。人类前端开发者可以凭直觉绕过后端开发者造成的文档错误,但 AI 不会宽容这些错误。即使 AI 函数 Schema 中只有很小的错误,也可能导致整个 AI 应用崩溃。
AI 从不原谅 Schema 错误
<?php
class BbsArticleController {
/**
* @OA\Post(
* path="/boards",
* tags={"BBS"},
* summary="Create a new article",
* description="Create a new article with its first snapshot",
* @OA\RequestBody(
* description="Article information to create",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* @OA\Property(
* property="title",
* type="string",
* description="Title of article",
* ),
* @OA\Property(
* property="content",
* type="string",
* description="Content body of article"
* ),
* @QA\Property(
* property="files",
* type="array",
* @QA\Items(
* @QA\Schema(
* @QA\Property(
* property="name",
* type="string",
* maxLength=255,
* description="File name, except the extension"
* ),
* @QA\Property(
* property="extension",
* type="string",
* nullable=true,
* maxLength=8,
* description="File extension. If no extension, then set null"
* ),
* @QA\Property(
* property="url",
* type="string",
* format="url",
* description="URL address that the file is located in"
* )
* )
* )
* )
* )
* )
* ),
* @OA\Response(response="200", description="Success"),
* @OA\Response(response="400", description="Fail")
* )
*/
public function create(Request $request);
}
?>
因此,AI 函数 Schema 必须由编译器构建。
如果你计划使用函数调用将后端服务器转换为 AI 智能体,请避开那些要求开发者手动编写 JSON Schema 的语言和框架,例如 PHP Laravel 和 Java Spring RestDocs。
应当改用由编译器驱动(或基于反射)的 OpenAPI 文档生成器,例如 Nestia 或 FastAPI。然后像下面这样,使用 @samchon/openapi 库将编译器生成的 OpenAPI 转换为特定的 AI 函数 Schema。
绝不要手写 AI 函数 Schema。
import { HttpLlm, IHttpLlmApplication } from "@samchon/openapi";
const app: IHttpLlmApplication = HttpLlm.application({
model: "claude",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((r) => r.json()),
});
由函数调用驱动的 AI 智能体开发,可以帮助开发者摆脱智能体工作流开发的困难与僵化。作为替代,开发者必须专注于为每个函数提供详尽的文档。
观察许多声称 Function Calling 和 MCP 未能按预期工作的其他 AI 开发者,我们发现,他们通常没有在文档上投入足够的精力,导致即使只有少量函数,函数调用也会失败。
相比之下,我们的商城智能体演示包含 289 个 API 函数,却没有任何重大问题,并且能够正常运行。两者的差异源于在文档上投入的精力不同。
下面是商城项目中的文档示例:
export function ShoppingSaleController<Actor extends IShoppingActorEntity>(
props: IShoppingControllerProps,
) {
@Controller(`shoppings/${props.path}/sales`)
abstract class ShoppingSaleController {
/**
* List up every summarized sales.
*
* List up every {@link IShoppingSale.ISummary summarized sales}.
*
* As you can see, returned sales are summarized, not detailed. It does not
* contain the SKU (Stock Keeping Unit) information represented by the
* {@link IShoppingSaleUnitOption} and {@link IShoppingSaleUnitStock} types.
* If you want to get such detailed information of a sale, use
* `GET /shoppings/customers/sales/{id}` operation for each sale.
*
* > If you're an A.I. chatbot, and the user wants to buy or compose
* > {@link IShoppingCartCommodity shopping cart} from a sale, please
* > call the `GET /shoppings/customers/sales/{id}` operation at least once
* > to the target sale to get detailed SKU information about the sale.
* > It needs to be run at least once for the next steps.
*
* @param input Request info of pagination, searching and sorting
* @returns Paginated sales with summarized information
* @tag Sale
*
* @author Samchon
*/
@TypedRoute.Patch()
public index(
@props.AuthGuard() actor: Actor,
@TypedBody() input: IShoppingSale.IRequest,
): Promise<IPage<IShoppingSale.ISummary>>;
/**
* Get a sale with detailed information.
*
* Get a {@link IShoppingSale sale} with detailed information including
* the SKU (Stock Keeping Unit) information represented by the
* {@link IShoppingSaleUnitOption} and {@link IShoppingSaleUnitStock} types.
*
* > If you're an A.I. chatbot, and the user wants to buy or compose a
* > {@link IShoppingCartCommodity shopping cart} from a sale, please call
* > this operation at least once to the target sale to get detailed SKU
* > information about the sale.
* >
* > It needs to be run at least once for the next steps. In other words,
* > if you A.I. agent has called this operation to a specific sale, you
* > don't need to call this operation again for the same sale.
* >
* > Additionally, please do not summarize the SKU information. Just show
* > the every options and stocks in the sale with detailed information.
*
* @param id Target sale's {@link IShoppingSale.id}
* @returns Detailed sale information
* @tag Sale
*
* @author Samchon
*/
@TypedRoute.Get(":id")
public at(
@props.AuthGuard() actor: Actor,
@TypedParam("id") id: string & tags.Format<"uuid">,
): Promise<IShoppingSale>;
}
return ShoppingSaleController;
}
编写函数描述时,你需要清晰全面地详细说明每个函数的用途。如果函数之间存在关系,如先决条件,你必须也描述这些。
/**
* 销售单位的最终库存信息。
*
* `IShoppingSaleUnitStock` 是 {@link IShoppingSaleUnit} 的从属实体,
* 代表待售的产品目录,是一种通过选择所属单位中的所有 {@link IShoppingSaleUnitSelectableOption 选项}
* (变量"select"类型)及其
* {@link IShoppingSaleUnitOptionCandidate 候选} 值而构建的最终库存。
* 它是客户实际购买的"商品"本身。
*
* - 产品名称)MacBook
* - 选项
* - CPU: { i3, i5, i7, i9 }
* - RAM: { 8GB, 16GB, 32GB, 64GB, 96GB }
* - SSD: { 256GB, 512GB, 1TB }
* - 最终库存数量: 4 * 5 * 3 = 60
*
* 作为参考,一个归属单位中 `IShoppingSaleUnitStock` 记录的总数可以用笛卡尔积获得。
* 换句话说,将每个(变量"select"类型)选项能拥有的所有候选值相乘得到的值,
* 就是该单位中最终库存的总数。
*
* 当然,如果没有单个变量"select"类型选项,该单位中的最终库存数量只有 1。
*
* @author Samchon
*/
export interface IShoppingSaleUnitStock { ... }
在描述类型时,将其写成 ERD(实体关系图)描述。
import { tags } from "typia";
/**
* 优惠券的限制信息。
*
* @author Samchon
*/
export interface IShoppingCouponRestriction {
/**
* 优惠券的访问级别。
*
* - public: 可从公开 API 查找
* - private: 无法从公开 API 查找
* - 由卖家或管理员任意分配
* - 从一次性链接发行
*/
access: "public" | "private";
/**
* 是否具有排他性。
*
* 排他性折扣优惠券是指与其他折扣优惠券具有排他关系,只能单独使用的折扣优惠券。
* 也就是说,当使用排他性折扣优惠券时,不能为同一个 {@link IShoppingOrder 订单} 或 {@link IShoppingOrderGood 商品} 使用任何其他折扣优惠券。
*
* 请注意,此排他属性是一个与乘法完全不同的概念,乘法是指同一优惠券是否可以
* 乘以并应用于同一订单的多个优惠券,所以请不要混淆它们。
*/
exclusive: boolean;
/**
* 发行的数量限制。
*
* 如果发行数量有限制,就不可能发行超过此值的票券。
*
* 换句话说,产生了 N 个优惠券以先到先得的方式发行的概念。
*/
volume: null | (number & tags.Type<"uint32">);
/**
* 每人发行的数量限制。
*
* 作为每人总发行数量的限制,通常分配 1 来限制向同一公民的重复发行,
* 或使用 NULL 值来设置无限制。
*
* 当然,通过分配 N 值,可以限制向同一公民发行的总数量。
*/
volume_per_citizen: null | (number & tags.Type<"uint32">);
/**
* 过期天数值。
*
* 折扣优惠券票券发行后 N 天过期的概念。
*
* 因此,客户必须在从发行时起 N 天内使用该票券,如果可能的话。
*/
expired_in: null | (number & tags.Type<"uint32">);
/**
* 过期日期。
*
* 折扣优惠券票券发行后 YYYY-MM-DD 后过期的概念。
*
* 可以与 expired_in 进行双重限制,其中使用过期日期较短的那个。
*/
expired_at: null | (string & tags.Format<"date-time">);
}
不要忘记描述属性。
过滤候选函数以减少上下文。
如果你把 Github MCP 服务器接入 Claude Desktop,很可能在许多情况下会崩溃。这是因为 Github MCP 服务器有 30 个函数,Claude Desktop 直接一次性将所有这些函数列出给智能体。太多函数会造成过多的上下文,导致幻觉。
为了防止这种幻觉,我们创建了一个选择器智能体,它根据每个函数的名称和描述来选择候选函数调用。只有由选择器智能体选择的候选函数才会呈现给主智能体,确保函数上下文不会像 Claude Desktop 那样变得压倒性。
在选择候选函数时,选择器智能体也会为其选择提供理由。此外,由于选择器智能体可以移除不再需要的候选函数,候选函数列表通常不超过四个。
Github MCP 函数过多导致大量崩溃
具有增强函数调用的 Agentica 工作正常
函数调用不是完美的,所以验证反馈是必不可少的。
在选择器智能体完成其工作后,调用器智能体将候选函数呈现给主智能体以进行实际函数调用。但是,调用器智能体还有一个额外的责任:验证反馈。
验证反馈是一种纠正类型错误参数的策略。如果 AI 智能体在为函数调用组合参数时犯了错误,调用器智能体会提供详细的类型错误信息,允许智能体在后续尝试中纠正错误。
例如,在购物商城智能体演示中,当使用 ShoppingCartCommodity 组合购物车来购买产品时,AI 智能体的错误率约为 60%。没有验证反馈,由函数调用驱动的 AI 智能体无法可靠地运行。
值 1 表示