我创建了一个MCP(模型上下文协议,Model Context Protocol,MCP)的替代方案。
我的替代方案是利用Swagger/OpenAPI和TypeScript类函数中的LLM函数调用功能,并通过编译器和验证反馈策略增强这些功能。借助这些策略,你可以用这些策略完全替换掉Anthropic Claude的MCP,转而使用更小的模型,比如gpt-4o-mini
(也可以换成本地的LLM)。
以下是我展示的另一种解决方案,在商场里搜索和购买商品。其后端服务器由289个API函数构成,并且仅通过gpt-4o-mini
模型中的8b
参数对话文本,我就可以调用所有这些API函数。
-
相关仓库
-
@samchon/openapi
: 将 Swagger/OpenAPI 转换为函数调用模式的工具 -
typia
:typia.llm.application<Class, Model>()
函数 -
@nestia
: 在 NestJS 中构建类型安全的 OpenAPI @agentica
: 利用上述工具构建的代理框架(即将推出,敬请期待)-
购物 AI 聊天机器人演示
-
@samchon/shopping-backend
: 由@nestia
构建的后端服务器 ShoppingChatApplication.tsx
: 由 React 构建的聊天应用代码
import { Agentica } from "@agentica/core";
import { HttpLlm, OpenApi } from "@samchon/openapi";
import typia from "typia";
const agent = new Agentica({
vendor: {
api: new OpenAI({ apiKey: "*****" }), // 你的API密钥
model: "gpt-4o-mini",
},
controllers: [
HttpLlm.application({
model: "chatgpt",
document: OpenApi.convert(
await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then(r => r.json())
)
}),
typia.llm.application<ShoppingCounselor, "chatgpt">(),
typia.llm.application<ShoppingPolicy, "chatgpt">(),
typia.llm.application<ShoppingSearchRag, "chatgpt">(),
],
});
await agent.对话("我想买MacBook Pro"); // 启动对话,用户想购买MacBook Pro
切换到全屏,退出全屏
调用函数脚本:
- 你能做什么?
- 能给我看看市场销售情况吗?
- 我想查看 MacBook(银色,16GB,1TB,英文系统)的库存详情,并把它加到购物车里
把购物车里的东西下单,我用现金支付,地址是 ~
请告诉我具体地址
LLM 选择合适的函数并调用它,填充参数。
- 功能调用指南 https://platform.openai.com/docs/guides/function-calling
- 结构化输出指南 https://platform.openai.com/docs/guides/structured-outputs
LLM(大语言模型)的功能调用特性意味着,LLM通过分析与用户的对话上下文来选择合适的功能并填充参数。还有一个相关概念叫结构化输出,即LLM自动将对话输出转换成像JSON这样的结构化数据格式。
我专注于这种大型语言模型的功能调用特性,并希望用户可以用它来完成所有任务。如果这样做,这样就可以完全替代Anthropic Claude的MCP(模型上下文协议),并适用于更小的模型,如gpt-4o-mini
。这是我希望通过我的解决方案实现的目标蓝图:
- 用户列出候选功能列表
- 用户不需要设计复杂的代理网络,也不需要设计工作流
- 我已经在我的网上商城中实现了这一点
将OpenAPI规范转换成LLM功能调用方案。
LLM的功能调用需要基于JSON模式的功能定义。然而,提供LLM服务的供应商并没有统一使用相同的JSON模式。“OpenAI GPT”和“Anthropic Claude”各自使用了不同的JSON规范来定义LLM功能调用,而Google的Gemini则采用了不同的规范。
更糟糕的是,Swagger/OpenAPI 文档采用的 JSON 模式规范与 LLM 函数调用的 JSON 模式规范不同,并且不同版本的 Swagger/OpenAPI 之间的规范差异很大。
为了处理这个问题,我制作了@samchon/openapi
。当它接收到Swagger/OpenAPI文档时,它会将其转换为增强的OpenAPI v3.1规范。然后将其转换为特定的服务供应商的LLM功能调用模式,从而绕过迁移模式直接转换为服务供应商的具体函数调用模式。需要注意的是,迁移模式是另一种中间件模式,用于将OpenAPI操作模式转换为类似于函数的模式。
此外,在将Swagger/OpenAPI文档转换为LLM函数调用模式时,@samchon/openapi
嵌入参数的运行时验证器,以支持#验证反馈策略。
import { FunctionCall } from "pseudo";
import { ILlmFunction, IValidation } from "typia";
export const correctFunctionCall = (p: {
call: FunctionCall;
functions: Array<ILlmFunction<"chatgpt">>;
retry: (reason: string, errors?: IValidation.IError[]) => Promise<unknown>;
}): Promise<unknown> => {
// 查找对应的函数
const func: ILlmFunction<"chatgpt"> | undefined =
p.functions.find((f) => f.name === p.call.name);
if (func === undefined) {
// 情况在我的经验中从未出现过
return p.retry(
"无法找到匹配的函数名称。请重试。",
);
}
// 验证
const result: IValidation<unknown> = func.validate(p.call.arguments);
if (result.success === false) {
// 第一次尝试:30%(比如 gpt-4o-mini 在商场聊天机器人中的情况)
// 第二次尝试,加上验证反馈:99%
// 第三次尝试,再加入验证反馈:从未失败
return p.retry(
"检测到类型错误。请根据验证错误进行修正",
{
errors: result.errors,
},
);
}
return result.data;
}
切换到全屏模式,退出全屏
难道LLM的功能调用完美吗?绝对不完美。
LLM(大型语言模型)服务提供商如OpenAI在调用函数或生成结构化输出时,经常会犯一些类型级别的错误,比如在函数参数的类型上。即使目标模式非常简单,比如Array<string>
类型,LLM也常常只是用一个string
类型的值来填充,而不是Array<string>
类型的值。
在我的经验中,OpenAI gpt-4o-mini
(拥有8b
参数的模型)在填充购物商场服务中的函数调用参数时,大约会犯70%类型的错误,这种错误通常出现在填充函数调用参数的过程中。为了克服大型语言模型(LLM)函数调用中的不足,许多LLM用户正在尝试使用更大的模型,例如llama-3.2-405b
,并采用简单的结构化模式。然而,我采用了验证反馈策略,而不是仅仅依赖于使用更大规模的模型或简化模式。这种方法的结果非常成功。
验证反馈策略的关键概念是,先让模型生成错误类型的参数,然后详细告诉模型具体的类型错误,从而诱导模型在下一次尝试中修正错误类型的参数。
我采用了typia.validate<T>()
和typia.llm.application<Class, Model>()
这两个函数来替代MCP(MCP)。它们构建的验证逻辑通过分析TypeScript源代码和类型,在编译阶段实现,从而使验证逻辑更为精确,因此比其他任何验证器都要更详细和准确。
这样的验证反馈策略与typia
运行时验证器结合使用,我能够完成最理想的LLM功能调用方式,从而完全替代MCP(模型上下文协议),适用于如gpt-4o-mini
这样的小型模型。通过这种方法,第一次功能调用的成功率从30%提高到了第二次尝试的99%,并且从第三次尝试开始没有再失败过。
组件 | typia |
TypeBox |
ajv |
io-ts |
zod |
C.V. |
---|---|---|---|---|---|---|
易用性 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
对象(简单) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
对象(分层) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
对象(递归) | ✔ | ❌ | ✔ | ✔ | ✔ | ✔ |
对象(联合,隐式) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
对象(联合,显式) | ✔ | ✔ | ✔ | ✔ | ✔ | ❌ |
对象(额外注释) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
对象(模板字符串类型) | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
对象(动态属性) | ✔ | ✔ | ✔ | ❌ | ❌ | ❌ |
数组(剩余元组) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
数组(分层) | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
数组(递归) | ✔ | ✔ | ✔ | ✔ | ✔ | ❌ |
数组(递归,联合) | ✔ | ✔ | ❌ | ✔ | ✔ | ❌ |
数组(R+U,隐式) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
数组(重复) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
数组(重复,联合) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
终极联合类型 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
-
C.V.
即class-validator
-
测试结构示例:
IShoppingSale
- 相关 ERD 图:ERD.md#sales
编译器的技巧完美地生成模式。
我觉得最重要的一点是,无论是Anthropic Claude的MCP,还是我提倡用来取代MCP的LLM功能调用策略,都是为了安全创建JSON模式。
顺便说一句,很多其他的AI开发者都在手动编写JSON模式。即使有一些工具帮助,这也是一种典型的重复定义,因此显得很危险。在传统的开发生态系统中,即使一个人在编写模式时犯了错误,另一个人也可以通过机构纠正它。但是,AI从不宽恕。
为了确保模式组合的安全,我一直在开发基于 TypeScript 编译器的 JSON 模式生成器 typia
和 @nestia
。当你使用 typia.llm.application<MyClass, "chatgpt">()
时,它会转换成一组 OpenAI 的函数调用模式。这种转换是在编译时通过分析 TypeScript 代码完成的,通过 typia
完成。
在开发用于生成OpenAPI文档的后端服务时,此外,@nestia
也会像 typia
一样工作。它会分析后端的TypeScript源代码,从而安全地生成无误的OpenAPI文档。
由编译器技能生成的如此完美的模式,我的替代方案比Anthropic Claude的MCP(模型上下文协议)更好。
ShoppingSaleController.ts
这是一个购物销售相关的控制器文件哦IShoppingSale
这是购物销售的接口结构文件哦
@Controller("shoppings/customers/sales")
export class ShoppingCustomerSaleController {
/**
* 获取包含详细信息的销售。
*
* 获取包含详细信息的销售,包括由 {@link IShoppingSaleUnitOption} 和
* {@link IShoppingSaleUnitStock} 类型表示的 SKU(库存保有单位,Stock Keeping Unit)信息。
*
* > 如果您是 A.I. 聊天机器人,而用户想要从销售记录中购买或组合一个
* > {@link IShoppingCartCommodity 购物车},那么请至少调用一次此操作以获取
* > 销售的详细 SKU 信息。
* >
* > 它需要至少运行一次才能进行下一步操作。换句话说,如果您作为 A.I. 代理
* > 已经为特定销售记录调用过此操作,那么无需再为相同的销售记录调用此操作。
* >
* > 此外,请不要总结 SKU 信息。只需显示销售中每个选项和库存的详细信息。
*
* @param id 目标销售的 {@link IShoppingSale.id}
* @returns 包含详细信息的销售
* @tag Sale
*/
@TypedRoute.Get(":id")
public async at(
@TypedParam("id") id: string & tags.Format<"uuid">,
): Promise<IShoppingSale>;
}
全屏 正常
传统的OpenAPI组合方法。
由韩国一家主要IT公司Toss Corporation编写。
> @ExtendWith(RestDocumentationExtension::class, SpringExtension::class)
> @SpringBootTest
> class SampleControllerTest {
> private lateinit var mockMvc: MockMvc
>
> @BeforeEach
> internal fun setUp(context: WebApplicationContext, restDocumentation: RestDocumentationContextProvider) {
> mockMvc = MockMvcBuilders.webAppContextSetup(context)
> .apply<DefaultMockMvcBuilder>(MockMvcRestDocumentation.documentationConfiguration(restDocumentation))
> .build()
> }
>
> @Test
> fun 获取样本ID测试() {
> val sampleId = "aaa"
> mockMvc.perform(
> get("/api/v1/samples/{sampleId}", sampleId)
> )
> .andExpect(status().isOk)
> .andExpect(jsonPath("sampleId", `is`(sampleId)))
> .andExpect(jsonPath("name", `is`("sample-$sampleId")))
> .andDo(
> MockMvcRestDocumentationWrapper.document(
> identifier = "sample",
> resourceDetails = ResourceSnippetParametersBuilder()
> .tag("Sample")
> .description("通过ID获取一个样本信息")
> .pathParameters(
> parameterWithName("sampleId")
> .description("样本的ID"),
> )
> .responseFields(
> fieldWithPath("sampleId")
> .type(JsonFieldType.STRING)
> .description("样本ID"),
> fieldWithPath("name")
> .type(JsonFieldType.STRING)
> .description("样本名称"),
> ),
> ),
> )
> }
> }
Agentica,代理式人工智能框架
(注:此处无图片描述。)
@nestia/agent
已迁移至@agentica/*
以增强功能并拆分为多个包以进一步扩展功能。由于它将在组织层面开发,你可能会更早地遇到它。
利用上述的OpenAPI、验证反馈和编译器策略,我和我的同伴们正在构建一个代理式AI框架。这可以完全替代MCP(模型上下文协议),并且使用比Anthropic Claude成本更低的模型。在我们的情况下,我们尝试使用本地的大语言模型。
新框架的名字是 @agentica
,预计将在 1 到 2 周后发布。功能部分已经差不多完成了,只剩下命令行工具包(CLI)和文档的编写。
并且@agentica
采用下面的多代理协调策略。通过这种代理协调策略,用户不需要创建复杂的代理图或工作流程,而是只需按顺序提供Swagger/OpenAPI文档或TypeScript类类型给@agentica
。@agentica
会自动处理所有函数调用。
当用户提到 @agentica
时,会将对话文本传递给 selector
,让 selector
在上下文中查找(或取消)候选功能。如果 selector
找不到任何可以调用的候选功能,且之前没有任何候选功能被选中,那么 selector
就会像普通的 ChatGPT 一样运行。
并且@agentica
进入一个循环,直到候选函数为空。在循环语句中,caller
代理通过分析用户对话来尝试调用LLM函数。如果上下文充足,可以组成候选函数的参数,caller
代理就调用了目标函数,然后让decriber
代理解释结果。否则,上下文不足,无法组成参数,caller
代理就会要求用户提供更多信息。
这种大语言模型(LLM)的功能调用策略,将 selector
、caller
和 describer
分隔,是 @agentica
中的核心逻辑。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章