👋 大家好,我是十三!
在上一篇《解构 Coze Studio:为 AI Agent 实现微型 DBaaS 的架构艺术》中,我们深入了 Agent 的"记忆系统"。但一个强大的 Agent 不仅要能"记住",更要能"做到"------它需要具备行动的能力,与外部世界进行交互,调用 API 来查询信息、发送通知或执行操作。
这就是今天的主角------Coze 的 plugin
模块,Agent 的"行动系统"。
Coze 是如何让 Agent 安全、可靠地调用外部 API 的?一个通用的插件体系,需要解决哪些核心技术挑战?
- 标准化:如何用一种统一的方式描述各式各样的 API?
- 安全性:如何安全地管理和使用第三方服务的密钥?
- 执行力:如何将大模型的"意图"精确地翻译成一个 HTTP 请求?
通过深入源码,我们发现 Coze 给出了一个极其优雅的答案:一个以 OpenAPI 规范为基石,集成了强大认证与智能执行引擎的插件架构。
1. 插件的"契约":基于 OpenAPI 3.0 的标准化设计
Coze 插件体系的基石,是对 OpenAPI 3.0 规范的全面拥抱。这并非简单的"支持",而是将其作为整个插件系统的通用语言和核心"契约"。无论是官方插件还是用户自定义的工具,其本质都被统一建模为一份 OpenAPI 文档。
这种选择,为整个系统带来了无与伦比的标准化和扩展性。
插件的"身份证":PluginManifest
每个插件都由一个 PluginManifest
(插件清单)来定义,它是插件的"身份证",描述了关于插件的一切元信息。
go
// api/model/crossdomain/plugin/plugin_manifest.go:32-42
type PluginManifest struct {
SchemaVersion string `json:"schema_version"`
NameForModel string `json:"name_for_model"`
NameForHuman string `json:"name_for_human"`
DescriptionForModel string `json:"description_for_model"`
DescriptionForHuman string `json:"description_for_human"`
// 核心:认证配置
Auth *AuthV2 `json:"auth"`
LogoURL string `json:"logo_url"`
// 核心:API 定义类型
API APIDesc `json:"api"`
// 核心:全局参数
CommonParams map[HTTPParamLocation][]*api.CommonParamSchema `json:"common_params"`
}
这份清单清晰地定义了:
- 给谁看 :
NameForModel
/DescriptionForModel
是给大模型看的,用于帮助模型理解插件功能,决定何时调用。NameForHuman
/DescriptionForHuman
则是展示给用户看的。 - 如何认证 :
Auth
字段详细定义了该插件所需的认证类型,我们将在下一节深入探讨。 - 通用参数 :
CommonParams
允许定义一些全局参数,例如,在每个请求的 Header 中自动加入User-Agent
。
工具的"说明书":ToolInfo 与 OpenAPI Operation
如果说 PluginManifest
是插件的"身份证",那么插件中每一个可执行的动作(Tool
),其"说明书"就是 OpenAPI 中的 Operation
对象。
go
// api/model/crossdomain/plugin/toolinfo.go:34-47
type ToolInfo struct {
ID int64
PluginID int64
Name string
Desc string
SubURL *string
Method *string
// ... 其他管理字段 ...
// 核心:OpenAPI 操作定义
Operation *Openapi3Operation `json:"operation"`
}
// Openapi3Operation is a type alias for openapi3.Operation
type Openapi3Operation = openapi3.Operation
Operation
字段(实际上就是 openapi3.Operation
)详细描述了 API 的一切:
Parameters
: 定义了 API 的所有参数,包括它们的位置(header
,query
,path
)、名称、类型和是否必需。RequestBody
: 定义了请求体(Request Body)的结构和媒体类型(如application/json
)。Responses
: 定义了可能的 HTTP 响应码及其对应的响应体结构。
通过这种方式,Coze 将外部世界五花八门的 API,全部统一到了 OpenAPI 这门"世界语言"之上。这正是其插件体系能够保持高度灵活性和扩展性的关键所在。
2. 安全的"守卫":深入多层次认证体系
当 Agent 需要与外部 API 交互时,安全是头等大事。一个设计拙劣的插件系统,可能会导致敏感凭证(Credentials)泄露,带来灾难性后果。
Coze 在 PluginManifest
中通过 AuthV2
字段,设计了一套非常灵活且周密的认证体系,能够覆盖绝大多数 API 的认证场景。
go
// api/model/crossdomain/plugin/plugin_manifest.go:44-53
type AuthV2 struct {
Type AuthType `json:"type"`
AuthorizationType AuthorizationType `json:"authorization_type"`
AuthorizationContent map[string]string `json:"authorization_content"`
CredentialLocation CredentialLocation `json:"credential_location"`
CustomHeaders map[string]string `json:"custom_headers"`
CredentialSchema *openapi3.SchemaRef `json:"credential_schema"`
OAuthProvider *OAuthProviderSchema `json:"oauth_provider"`
}
type AuthType string
const (
AuthTypeNone AuthType = "none"
AuthTypeServiceToken AuthType = "service_token" // API Key
AuthTypeOAuth AuthType = "oauth"
)
AuthV2
结构的核心是 Type
字段,它像一个开关,定义了三种截然不同的认证模式:None
、Service Token
和 OAuth
。
模式一:AuthTypeNone
- 开放的门户
这是最简单的一种模式,适用于那些不需要任何认证的公开 API(例如,查询天气、获取公开资讯等)。当 type
设置为 none
时,Coze 在调用 API 时不会附加任何认证信息。
模式二:AuthTypeServiceToken
- API Key 认证
这是最常见的 API 认证方式,即通过 API Key(或称为 Bearer Token、Service Token)进行认证。
在这种模式下,Coze 的设计非常灵活:
AuthorizationType
: 定义了 Key 的类型,通常是bearer
或basic
。CredentialLocation
: 定义了 Key 应该被放置在请求的哪个位置,是header
还是query
。AuthorizationContent
: 这是一个map[string]string
,允许你精确地指定 Header 或 Query 参数的Key-Value
对。例如,如果 Key 在 Header 中,你可以定义 Key 为Authorization
,Value 为Bearer {{bstudio_api_key}}
。
这里的 {{bstudio_api_key}}
是一个占位符,Coze 会在实际请求时,将用户为该插件配置的安全凭证动态地填充进去。这种"定义"与"凭证"分离的设计,确保了插件定义的模板是通用的,而敏感信息则被安全地隔离存储。
模式三:AuthTypeOAuth
- 授权的艺术
对于需要代表用户操作资源的场景(例如,读取用户的 Google Drive 文件、以用户的名义发布一条推文),OAuth 2.0 是事实上的标准。Coze 对此提供了完整的支持,并且实现了最经典的授权码模式(Authorization Code Grant)。
当 type
设置为 oauth
时,OAuthProvider
字段会被启用。
- 获取授权 URL (
GetOAuthURL
): 根据OAuthProvider
中定义的AuthorizationURL
、ClientID
和Scope
等参数,生成一个引导用户跳转的授权页面 URL。 - 交换 Code 获取 Token (
OAuthCode
): 用户在第三方平台授权后,会被重定向回 Coze,并附带一个code
。此函数负责用这个code
向TokenURL
交换,获取access_token
和refresh_token
。 - 刷新 Token (
refreshToken
):access_token
通常有较短的有效期。当它过期时,Coze 会自动使用refresh_token
去获取一个新的access_token
,对用户和上层应用保持透明。 - 凭证注入 : 获取到
access_token
后,其注入方式与 API Key 模式类似,同样由CredentialLocation
和AuthorizationContent
来定义。
整个过程可以用下面的序列图来概括:
OAuth Provider" User->>+CozeBackend: 1. 触发插件的 OAuth 连接 CozeBackend->>-User: 2. 返回服务商授权 URL User->>+AuthProvider: 3. 跳转至授权页
(登录并授予权限) AuthProvider->>-User: 4. 重定向回 Coze
并附带 authorization_code User->>+CozeBackend: 5. 将 authorization_code 发送给 Coze CozeBackend->>+AuthProvider: 6. 用 code 交换 token
(发送 code + client_secret) AuthProvider->>-CozeBackend: 7. 返回 access_token & refresh_token CozeBackend->>CozeBackend: 8. 安全存储用户凭证
通过这套精巧的认证机制,Coze 为 Agent 连接外部世界提供了坚实的安全保障,无论是简单的公开 API,还是复杂的、需要用户授权的场景,都能从容应对。
3. 智能执行引擎:Tool Calling 的实现艺术
认证机制保证了"谁可以调用",而执行引擎则决定了"如何调用"。这是整个插件体系的核心:如何将大模型输出的"意图"(Tool Calling 参数),精确地转换为一个可执行的 HTTP 请求?
Coze 的解决方案体现在 ExecuteTool
这一核心领域服务中,它是连接 AI "思维"与现实世界"动作"的桥梁。
执行流程的精巧设计
go
// domain/plugin/service/exec_tool.go:25-40
func (p *pluginService) ExecuteTool(ctx context.Context, req *entity.ExecuteToolRequest) (*entity.ExecuteToolResponse, error) {
// 1. 构建工具执行器 - 获取正确的插件和工具版本
toolExecutor, err := p.buildToolExecutor(ctx, req)
if err != nil {
return nil, err
}
// 2. 预处理参数 - 将 LLM 的 JSON 参数转换为结构化对象
arguments, err := p.preprocessArgumentsInJson(ctx, toolExecutor.Tool.Operation, req.Arguments)
if err != nil {
return nil, err
}
// 3. 构建 HTTP 请求 - 将参数映射到具体的 HTTP 请求
httpReq, err := p.buildHTTPRequest(ctx, toolExecutor, arguments)
if err != nil {
return nil, err
}
// 4. 注入认证信息 - 根据插件的认证配置添加凭证
if err := p.injectAuthInfo(ctx, httpReq, toolExecutor); err != nil {
return nil, err
}
// 5. 执行 HTTP 请求并返回结果
return p.executeHTTPRequest(ctx, httpReq)
}
这个流程的每一步都体现了精心的设计思考,我们可以将其可视化为如下的流程图:
选择插件版本 (草稿/线上)"] --> B["2. preprocessArguments
解析 & 验证 LLM 参数"] B --> C["3. buildHTTPRequest
构建 HTTP 请求 (URL, Body, Header)"] C --> D["4. injectAuthInfo
注入认证信息 (API Key/OAuth)"] D --> E["5. executeHTTPRequest
执行请求并返回结果"] end
第一步:版本选择的智慧 - buildToolExecutor
不同的使用场景(调试、预览、生产)需要使用不同版本的插件。buildToolExecutor
函数根据执行上下文的 Scene
(场景)字段,智能地选择合适的插件版本:
go
// 根据执行场景选择插件版本
switch req.Scene {
case entity.SceneDraft:
// 开发调试时使用草稿版本
pluginInfo = draftPlugin
case entity.SceneOnline:
// 生产环境使用线上版本
pluginInfo = onlinePlugin
}
这种设计确保了开发者可以安全地在草稿环境中测试新功能,而不影响线上用户的体验。
第二步:参数解析的艺术 - preprocessArgumentsInJson
大模型输出的 Tool Calling 参数是一个 JSON 字符串,而 OpenAPI 规范中定义的参数却有着丰富的类型和位置信息。这一步的关键是将 JSON 中的"扁平"参数,映射到 OpenAPI Operation
中定义的具体参数结构。
go
// domain/plugin/service/exec_tool.go:85-120
func (p *pluginService) preprocessArgumentsInJson(ctx context.Context, operation *openapi3.Operation, argumentsJson string) (map[string]interface{}, error) {
var arguments map[string]interface{}
if err := json.Unmarshal([]byte(argumentsJson), &arguments); err != nil {
return nil, fmt.Errorf("invalid arguments JSON: %w", err)
}
// 遍历 OpenAPI Operation 中定义的所有参数
for _, param := range operation.Parameters {
paramName := param.Value.Name
if value, exists := arguments[paramName]; exists {
// 根据参数的 Schema 进行类型验证和转换
if err := p.validateAndConvertParam(param.Value, value); err != nil {
return nil, fmt.Errorf("parameter %s validation failed: %w", paramName, err)
}
} else if param.Value.Required {
return nil, fmt.Errorf("required parameter %s is missing", paramName)
}
}
return arguments, nil
}
这个过程不仅仅是简单的 JSON 解析,还包括:
- 类型验证:确保参数值符合 OpenAPI Schema 定义的类型约束
- 必填校验 :检查所有
required: true
的参数是否都已提供 - 默认值处理:为未提供的可选参数填充默认值
第三步:请求构建的精确映射 - buildHTTPRequest
这是整个执行流程中最为复杂的一步:将解析后的参数,根据 OpenAPI 规范精确地映射到 HTTP 请求的各个部分。
go
// domain/plugin/service/exec_tool.go:140-200
func (p *pluginService) buildHTTPRequest(ctx context.Context, executor *ToolExecutor, arguments map[string]interface{}) (*http.Request, error) {
// 构建完整的请求 URL
fullURL := executor.Plugin.Manifest.API.BaseURL + *executor.Tool.SubURL
// 处理路径参数 (Path Parameters)
for paramName, paramValue := range arguments {
if isPathParam(paramName, executor.Tool.Operation) {
fullURL = strings.ReplaceAll(fullURL, "{"+paramName+"}", fmt.Sprintf("%v", paramValue))
}
}
// 构建查询参数 (Query Parameters)
queryParams := url.Values{}
for paramName, paramValue := range arguments {
if isQueryParam(paramName, executor.Tool.Operation) {
queryParams.Add(paramName, fmt.Sprintf("%v", paramValue))
}
}
// 构建请求体 (Request Body)
var requestBody io.Reader
if executor.Tool.Operation.RequestBody != nil {
bodyData := extractRequestBodyData(arguments, executor.Tool.Operation.RequestBody)
jsonBody, _ := json.Marshal(bodyData)
requestBody = bytes.NewBuffer(jsonBody)
}
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, *executor.Tool.Method, fullURL, requestBody)
if err != nil {
return nil, err
}
// 设置请求头 (Headers)
for paramName, paramValue := range arguments {
if isHeaderParam(paramName, executor.Tool.Operation) {
req.Header.Set(paramName, fmt.Sprintf("%v", paramValue))
}
}
return req, nil
}
这个函数展现了 OpenAPI 规范的强大之处:通过标准化的参数位置描述(in: path
、in: query
、in: header
),Coze 能够机械而精确地将任意参数放置到 HTTP 请求的正确位置。
第四步:安全注入 - injectAuthInfo
最后一步是将之前介绍的认证信息注入到构建好的 HTTP 请求中。
go
// domain/plugin/service/exec_tool.go:220-250
func (p *pluginService) injectAuthInfo(ctx context.Context, req *http.Request, executor *ToolExecutor) error {
auth := executor.Plugin.Manifest.Auth
if auth == nil || auth.Type == AuthTypeNone {
return nil // 无需认证
}
switch auth.Type {
case AuthTypeServiceToken:
return p.injectServiceTokenAuth(ctx, req, auth, executor.Plugin.ID)
case AuthTypeOAuth:
return p.injectOAuthAuth(ctx, req, auth, executor.Plugin.ID)
default:
return fmt.Errorf("unsupported auth type: %s", auth.Type)
}
}
func (p *pluginService) injectServiceTokenAuth(ctx context.Context, req *http.Request, auth *AuthV2, pluginID int64) error {
// 从安全存储中获取用户为该插件配置的 API Key
credentials, err := p.credentialRepo.GetCredentials(ctx, pluginID)
if err != nil {
return fmt.Errorf("failed to get credentials: %w", err)
}
// 根据配置的位置和格式注入认证信息
for location, content := range auth.AuthorizationContent {
// 将占位符 {{bstudio_api_key}} 替换为实际的凭证
actualContent := strings.ReplaceAll(content, "{{bstudio_api_key}}", credentials.APIKey)
switch auth.CredentialLocation {
case CredentialLocationHeader:
req.Header.Set(location, actualContent)
case CredentialLocationQuery:
q := req.URL.Query()
q.Set(location, actualContent)
req.URL.RawQuery = q.Encode()
}
}
return nil
}
这种设计的精妙之处在于配置与实现的分离 :插件作者只需在 PluginManifest
中声明"认证信息应该如何被使用"(位置、格式),而具体的凭证获取、注入逻辑则由 Coze 统一处理,既保证了灵活性,又确保了安全性。
错误处理与可观测性
值得一提的是,Coze 在执行引擎中还实现了完善的错误处理和日志记录机制。每一步的执行结果都会被详细记录,包括:
- 参数解析错误的具体位置和原因
- HTTP 请求构建的详细信息
- 第三方 API 的响应状态和内容
- 认证失败的具体错误类型
这为插件的调试和运维提供了强有力的支持。
4. 草稿与线上的双版本哲学
与 Coze 的内存模块类似,插件体系同样采用了"草稿"(Draft)与"线上"(Online)双版本的设计哲学。这种设计并非简单的版本管理,而是体现了对安全部署 和持续迭代深层次的思考。
实体层面的双版本设计
go
// domain/plugin/entity/plugin.go:15-25
type PluginInfo struct {
ID int64 `json:"id"`
CreatorID int64 `json:"creator_id"`
Name string `json:"name"`
Description string `json:"description"`
Version PluginVersion `json:"version"` // Draft 或 Online
Status PluginStatus `json:"status"`
Manifest *PluginManifest `json:"manifest"`
// 版本关联
DraftID *int64 `json:"draft_id,omitempty"`
OnlineID *int64 `json:"online_id,omitempty"`
Tools []*ToolInfo `json:"tools,omitempty"`
}
type PluginVersion string
const (
PluginVersionDraft PluginVersion = "draft"
PluginVersionOnline PluginVersion = "online"
)
每个插件在物理存储上实际存在两份独立的记录:
- 草稿版本 :
version = "draft"
,用于开发、测试和预览 - 线上版本 :
version = "online"
,用于生产环境的稳定运行
DraftID
和 OnlineID
字段建立了两个版本之间的关联关系,形成了一种"影子版本"的架构模式。
版本管理的核心逻辑
go
// application/plugin/plugin.go:150-180
func (p *PluginApplicationService) PublishPlugin(ctx context.Context, req *plugin.PublishPluginRequest) (*plugin.PublishPluginResponse, error) {
// 1. 获取草稿版本插件
draftPlugin, err := p.pluginService.GetPluginByID(ctx, req.DraftPluginID)
if err != nil {
return nil, fmt.Errorf("draft plugin not found: %w", err)
}
if draftPlugin.Version != entity.PluginVersionDraft {
return nil, fmt.Errorf("plugin %d is not a draft version", req.DraftPluginID)
}
// 2. 检查是否已有线上版本
if draftPlugin.OnlineID != nil {
// 更新现有线上版本
return p.updateOnlinePlugin(ctx, draftPlugin)
} else {
// 创建新的线上版本
return p.createOnlinePlugin(ctx, draftPlugin)
}
}
func (p *PluginApplicationService) createOnlinePlugin(ctx context.Context, draftPlugin *entity.PluginInfo) (*plugin.PublishPluginResponse, error) {
// 创建线上版本的插件记录
onlinePlugin := &entity.PluginInfo{
CreatorID: draftPlugin.CreatorID,
Name: draftPlugin.Name,
Description: draftPlugin.Description,
Version: entity.PluginVersionOnline,
Status: entity.PluginStatusActive,
Manifest: draftPlugin.Manifest, // 复制配置
DraftID: &draftPlugin.ID, // 建立关联
}
// 保存线上版本
createdOnline, err := p.pluginService.CreatePlugin(ctx, onlinePlugin)
if err != nil {
return nil, err
}
// 反向更新草稿版本的 OnlineID
draftPlugin.OnlineID = &createdOnline.ID
if err := p.pluginService.UpdatePlugin(ctx, draftPlugin); err != nil {
return nil, err
}
// 同步创建线上版本的工具
return p.syncToolsToOnline(ctx, draftPlugin.ID, createdOnline.ID)
}
这个发布流程展现了双版本架构的核心价值:
- 安全隔离:草稿版本的任何修改都不会影响正在生产环境中运行的线上版本
- 原子发布:整个发布过程是原子性的,要么完全成功,要么完全回滚
- 双向关联 :通过
DraftID
和OnlineID
建立的双向指针,使得版本间的追溯变得非常简单
这个过程的内在逻辑可以通过下面的序列图清晰地展现:
(复制草稿配置) Repository-->>-DomainService: 成功 DomainService-->>-AppService: 返回更新后的线上插件 else 无线上版本 AppService->>+DomainService: 2b. createOnlinePlugin(draftPlugin) DomainService->>+Repository: 创建新线上版本记录
(复制配置, 关联 draft_id) Repository-->>-DomainService: 返回带 ID 的新线上插件 DomainService-->>-AppService: 返回新线上插件实体 AppService->>+DomainService: 3. UpdatePlugin(draftPlugin with online_id) DomainService->>+Repository: 更新草稿版本记录
(回填 online_id) Repository-->>-DomainService: 成功 DomainService-->>-AppService: 成功 end
执行时的版本选择策略
回到之前分析的 ExecuteTool
函数,版本选择的逻辑变得清晰明了:
go
// domain/plugin/service/exec_tool.go:45-65
func (p *pluginService) buildToolExecutor(ctx context.Context, req *entity.ExecuteToolRequest) (*ToolExecutor, error) {
var pluginInfo *entity.PluginInfo
var toolInfo *entity.ToolInfo
switch req.Scene {
case entity.SceneDraft:
// 预览/调试场景:使用草稿版本
pluginInfo, err = p.GetPluginByID(ctx, req.PluginID)
if err != nil {
return nil, err
}
if pluginInfo.Version != entity.PluginVersionDraft {
return nil, fmt.Errorf("plugin %d is not a draft version", req.PluginID)
}
case entity.SceneOnline:
// 生产场景:使用线上版本
pluginInfo, err = p.GetOnlinePluginByDraftID(ctx, req.PluginID)
if err != nil {
return nil, fmt.Errorf("no online version found for plugin %d", req.PluginID)
}
if pluginInfo.Version != entity.PluginVersionOnline {
return nil, fmt.Errorf("plugin %d is not an online version", pluginInfo.ID)
}
}
// 根据选定的插件版本,获取对应的工具定义
toolInfo, err = p.GetToolByPluginAndName(ctx, pluginInfo.ID, req.ToolName)
if err != nil {
return nil, fmt.Errorf("tool %s not found in plugin %d", req.ToolName, pluginInfo.ID)
}
return &ToolExecutor{
Plugin: pluginInfo,
Tool: toolInfo,
}, nil
}
这种场景驱动的版本选择机制,确保了:
- 开发者体验:在 Coze Studio 的插件编辑器中测试时,自动使用草稿版本
- 用户体验:Agent 在实际对话中调用工具时,始终使用稳定的线上版本
- 灰度发布:可以让特定用户群体先体验草稿版本,验证无误后再全量发布
这种设计在复杂的生产环境中具有极高的实用价值,它不仅保证了系统的稳定性,还为持续集成和持续部署(CI/CD)提供了天然的支持。
5. 架构层次的优雅流转
让我们再次回到整洁架构的视角,看看一个完整的 Tool Calling 请求是如何在 Coze 的各个层次间优雅流转的。这个过程完美地诠释了依赖倒置原则 和关注点分离的威力。
请求的生命周期
以"Agent 调用天气查询插件"为例,描绘一个 Tool Calling 请求的完整生命周期:
1. 接口层 (api
) - 请求的入口
swift
POST /v1/plugin/tool/execute
{
"plugin_id": 12345,
"tool_name": "get_weather",
"arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}",
"scene": "online"
}
go
// api/handler/plugin/plugin.go
func (h *PluginHandler) ExecuteTool(ctx context.Context, c *app.RequestContext) {
var req plugin.ExecuteToolRequest
if err := c.BindAndValidate(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
return
}
// 调用应用层服务
resp, err := h.pluginAppService.ExecuteTool(ctx, &req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, resp)
}
接口层的职责非常纯粹:适配和转换。它将 HTTP 请求转换为应用层能够理解的结构化对象,并将应用层的响应转换回 HTTP 格式。
2. 应用层 (application
) - 用例的编排
go
// application/plugin/plugin.go:200-220
func (p *PluginApplicationService) ExecuteTool(ctx context.Context, req *plugin.ExecuteToolRequest) (*plugin.ExecuteToolResponse, error) {
// 权限检查
if err := p.checkExecutePermission(ctx, req.PluginID, req.UserID); err != nil {
return nil, fmt.Errorf("permission denied: %w", err)
}
// 构建领域层请求
domainReq := &entity.ExecuteToolRequest{
PluginID: req.PluginID,
ToolName: req.ToolName,
Arguments: req.Arguments,
Scene: entity.ExecutionScene(req.Scene),
UserID: req.UserID,
}
// 调用领域服务
domainResp, err := p.pluginService.ExecuteTool(ctx, domainReq)
if err != nil {
return nil, err
}
// 记录执行日志
p.logService.LogToolExecution(ctx, &log.ToolExecutionEvent{
PluginID: req.PluginID,
ToolName: req.ToolName,
UserID: req.UserID,
Success: domainResp.Success,
Duration: domainResp.Duration,
})
// 转换为应用层响应
return &plugin.ExecuteToolResponse{
Success: domainResp.Success,
Result: domainResp.Result,
Error: domainResp.Error,
}, nil
}
应用层体现了用例编排的思想:它不包含核心业务逻辑,而是负责协调多个领域服务(插件服务、权限服务、日志服务)来完成一个完整的应用场景。
3. 领域层 (domain
) - 业务逻辑的核心
这就是我们之前深入分析的 ExecuteTool
函数所在的层次。领域层专注于纯粹的业务逻辑:
- 版本选择策略
- 参数解析和验证
- HTTP 请求构建
- 认证信息注入
它通过仓储接口与数据持久化交互,通过领域事件与其他限界上下文通信,但对具体的技术实现一无所知。
4. 基础设施层 (infra
) - 技术实现的细节
go
// infra/impl/plugin/plugin_repo.go
type pluginRepository struct {
db *gorm.DB
}
func (r *pluginRepository) GetPluginByID(ctx context.Context, id int64) (*entity.PluginInfo, error) {
var plugin model.Plugin
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&plugin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("plugin %d not found", id)
}
return nil, err
}
return r.convertToEntity(&plugin), nil
}
func (r *pluginRepository) convertToEntity(model *model.Plugin) *entity.PluginInfo {
return &entity.PluginInfo{
ID: model.ID,
CreatorID: model.CreatorID,
Name: model.Name,
Description: model.Description,
Version: entity.PluginVersion(model.Version),
Status: entity.PluginStatus(model.Status),
Manifest: r.parseManifest(model.ManifestJSON),
DraftID: model.DraftID,
OnlineID: model.OnlineID,
}
}
基础设施层处理所有"脏活累活":
- 数据库查询和事务管理
- HTTP 客户端的具体实现
- 缓存策略和连接池管理
- 第三方服务的集成
它实现了领域层定义的所有接口,但绝不会向上"泄露"任何技术细节。
依赖注入的魔法
这种层次分明的架构,依赖于一套精心设计的依赖注入机制:
go
// application/application.go:30-50
type Application struct {
PluginAppService *plugin.PluginApplicationService
ConversationAppService *conversation.ConversationApplicationService
// ... 其他应用服务
}
func NewApplication(
pluginService domain_plugin.PluginService,
conversationService domain_conversation.ConversationService,
// ... 其他领域服务
) *Application {
return &Application{
PluginAppService: plugin.NewPluginApplicationService(
pluginService,
// 注入其他依赖的领域服务
),
ConversationAppService: conversation.NewConversationApplicationService(
conversationService,
// 注入其他依赖的领域服务
),
}
}
通过这种方式,每一层都只依赖于接口 而非实现,整个系统的耦合度降到了最低,可测试性达到了最高。
这种架构设计的优雅之处在于:无论插件系统如何复杂化,无论需要支持多少种认证方式,无论需要集成多少个第三方服务,核心的业务逻辑始终保持清晰和稳定。
插件体系的架构艺术
通过深入 Coze 插件模块的源码,我们见证了一个生产级 AI Agent 平台是如何解决"连接外部世界"这一复杂挑战的。
Coze 的插件体系展现了三个层面的架构艺术:
标准化的力量
通过全面拥抱 OpenAPI 3.0 规范,Coze 将千变万化的外部 API 统一到了一门"世界语言"之上。这种标准化不仅大大降低了系统的复杂度,更为整个生态的扩展提供了坚实的基础。
安全的智慧
从简单的 API Key 到复杂的 OAuth 2.0 流程,AuthV2
体系展现了对 API 安全深刻的理解。配置与实现分离的设计哲学,既保证了灵活性,又确保了凭证的安全管理。
架构的优雅
在 AI Agent 技术飞速发展的今天,优秀的软件架构已经成为了决定平台竞争力的关键因素。Coze 的插件体系,无疑为我们提供了一个极具参考价值的架构范本。
正如《整洁架构》中所说:"好的架构让系统易于理解、易于开发、易于维护、易于部署。"而 Coze 的插件架构,正是这一理念的完美诠释。
👨💻 关于十三Tech
资深服务端研发工程师,AI 编程实践者。
专注分享真实的技术实践经验,相信 AI 是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!