数据截至 (上游 commit 9fe68b96f0a7)
Composio — Provider 适配器
本章讲什么: Composio 最优雅的设计——怎么用「一个
wrapTool方法」把同一份工具数据适配到 OpenAI、LangChain、Vercel 等任意框架,而核心 SDK 完全不认识这些框架。以及为什么 provider 分「agentic / non-agentic」两类。
1. 它要解决的小问题
每个 agent 框架的工具格式都不一样:
| 框架 | 工具长什么样 |
|---|---|
| OpenAI | { type:'function', function:{ name, description, parameters } } |
| LangChain | new DynamicStructuredTool({ name, schema, func })(schema 是 zod) |
| Vercel AI SDK | tool({ parameters, execute }) |
如果核心 SDK 直接产某一种格式,换框架就得重写。Composio 的解法:核心只产「中立工具数据」,把「翻译成框架格式」这件事外包给 Provider。
2. 思路 / 直觉:适配器模式
类比: Provider 就是「插头转换器」。Composio 后端是「220V 电源」(中立工具数据),OpenAI / LangChain 是不同国家的「插座标准」。你出国不用换电器,换个转换头就行。
核心 SDK 只跟一个抽象接口打交道:「给你一批工具,你 wrap 成你的格式」。具体怎么 wrap,各 provider 自己实现。
Composio 中立工具数据 (Tool[])
│
┌───────────┼───────────┐
▼ ▼ ▼
OpenAIProvider LangchainP. VercelP.
.wrapTool() .wrapTool() .wrapTool()
│ │ │
▼ ▼ ▼
OpenAI 格式 LangChain 格式 Vercel 格式
3. 两类 Provider:agentic vs non-agentic
这是理解 provider 的关键分叉。差别在于「谁负责真正执行工具」。
| non-agentic | agentic | |
|---|---|---|
| 代表 | OpenAI(裸 chat completions) | LangChain、Vercel、OpenAI Agents |
| 框架会自己跑工具吗 | 不会,只产 tool schema 给模型 | 会,框架内部有 agent loop |
wrapTool 签名 | wrapTool(tool) | wrapTool(tool, executeTool) |
| 执行谁管 | 你手动调 provider.handleToolCalls() | 框架回调 executeTool 自动执行 |
这个分叉直接刻在基类里(provider/BaseProvider.ts:134-181):
export abstract class BaseNonAgenticProvider<...> extends BaseProvider<...> {
override readonly _isAgentic = false;
abstract wrapTool(tool: Tool): TTool; // ← 不收执行函数
abstract wrapTools(tools: Tool[]): TToolCollection;
}
export abstract class BaseAgenticProvider<...> extends BaseProvider<...> {
override readonly _isAgentic = true;
abstract wrapTool(tool: Tool, executeTool: ExecuteToolFn): TTool; // ← 收执行函数
abstract wrapTools(tools: Tool[], executeTool: ExecuteToolFn): TToolCollection;
}
注意 wrapTool 签名差一个参数 executeTool:agentic provider 在 wrap 时就把执行函数缝进工具里,这样框架的 agent loop 选中工具后能直接调;non-agentic 不需要,因为执行由你显式触发。
4. non-agentic 实例:OpenAIProvider(默认)
OpenAIProvider 是 SDK 自带的默认 provider(composio.ts:317)。它的 wrapTool 极简——纯格式转换,不碰执行(provider/OpenAIProvider.ts:99-109):
override wrapTool = (tool: Tool): OpenAiTool => {
return {
type: 'function',
function: {
name: tool.slug,
description: tool.description,
parameters: tool.inputParameters, // 直接用 Composio 的 JSON schema
},
};
};
因为 OpenAI 裸 API 不自己跑工具,执行得你手动触发。Provider 为此提供 handleToolCalls:给它一个 chat completion 响应,它解析出工具调用、逐个执行、返回 tool 消息(provider/OpenAIProvider.ts:256-268)。执行时它调的是基类的 executeTool,后者最终走回 Tools.execute。
执行函数从哪来?
Tools 构造时把全局执行函数注入给 provider(models/Tools.ts:142):
this.provider._setExecuteToolFn(this.createExecuteFnForProviders());
基类的 executeTool 就是这个注入函数的薄封装(provider/BaseProvider.ts:55-66)——这样 provider 不直接依赖 Tools,核心与 provider 解耦。
5. agentic 实例:LangchainProvider
LangChain 自己有 agent executor,会主动跑工具。所以它的 wrapTool 收第二个参数 executeTool,并把它包成 LangChain 工具的 func(ts/packages/providers/langchain/src/index.ts:124-150):
wrapTool(tool: Tool, executeTool: ExecuteToolFn): DynamicStructuredTool {
const func = async (...args: unknown[]): Promise<unknown> => {
// 模型偶尔把入参发成 JSON 字符串而非对象,normalize 容错(issue #2406)
const result = await executeTool(tool.slug, normalizeToolArguments(args[0], tool.slug));
return JSON.stringify(result);
};
const parameters = jsonSchemaToZodSchema(tool.inputParameters); // JSON schema → zod
return new DynamicStructuredTool({ name: tool.slug, description, schema: parameters, func });
}
两个看点:
executeTool缝进func:LangChain 一旦选中工具就调func,自动打回 Composio 执行。jsonSchemaToZodSchema:LangChain 要 zod schema,而 Composio 给的是 JSON schema,所以要转一道(这个转换器本身在ts/packages/core/src/utils/jsonSchema.ts:329)。
6. 原理演示:写一个最小 provider
下面这段演示「实现一个自定义 non-agentic provider 要写什么」——核心就是 wrapTool/wrapTools:
// 示意,非源码:把工具转成「给某个假想框架」的格式
import { BaseNonAgenticProvider, Tool } from '@composio/core';
class MyProvider extends BaseNonAgenticProvider<MyTool[], MyTool> {
readonly name = 'my-framework';
wrapTool(tool: Tool): MyTool {
return { id: tool.slug, schema: tool.inputParameters }; // 翻译成你框架的格式
}
wrapTools(tools: Tool[]): MyTool[] {
return tools.map(t => this.wrapTool(t)); // 几乎总是 map 一遍
}
}
// 执行时:在拿到模型的工具调用后,调 this.executeTool(slug, {userId, arguments})
重点看:你只需关心「格式翻译」,执行能力由基类的 executeTool 免费给你。
7. 巧妙之处
- 核心零框架依赖:
@composio/core不 import 任何 agent 框架;框架 SDK 反过来 import core。所以加新框架支持 = 新写一个 provider 包,核心一行不动。 - agentic 标志驱动遥测与执行策略:
_isAgentic一个布尔位,既告诉遥测「这是 agentic 流程」(composio.ts:384),又决定 wrapTool 是否注入执行函数。一个标志贯穿两处行为。 - 执行函数注入而非继承:provider 通过
_setExecuteToolFn被动接收执行能力,而不是继承Tools。这让 provider 可以单独测试、单独发包。
8. 边界与局限
- non-agentic provider 的工具不会自动执行——你必须自己调
handleToolCalls之类的 helper,忘了就只有 schema、没有动作。 wrapTool是同步纯转换,不能在这里做异步取数据;要异步预处理得用modifySchema(见 01)。
9. 横向对比
- 这套「适配器 + 注入执行函数」与 LangChain 的 tool 抽象思路相通,但 Composio 把它抬到「跨框架」层级:同一份工具数据可同时供 N 个框架。
- 与 MCP(Model Context Protocol) 相比:MCP 是「协议层」标准,Composio provider 是「SDK 层」适配;两者不冲突,Composio 也能把会话暴露成 MCP endpoint(见 04)。
10. 代码地图(导航索引)
| 主题 | 文件 | 关键符号 |
|---|---|---|
| Provider 基类 | ts/packages/core/src/provider/BaseProvider.ts | BaseNonAgenticProvider、BaseAgenticProvider、executeTool、_setExecuteToolFn |
| 默认 provider | ts/packages/core/src/provider/OpenAIProvider.ts | OpenAIProvider.wrapTool、handleToolCalls、executeToolCall |
| agentic 范例 | ts/packages/providers/langchain/src/index.ts | LangchainProvider.wrapTool |
| JSON→zod 转换 | ts/packages/core/src/utils/jsonSchema.ts:329 | jsonSchemaToZodSchema |
| Python provider 基类 | python/composio/core/provider/base.py | BaseProvider(Python) |