Eino Skill 机制架构分析
概述
本文档分析 eino 框架中 Skill 机制的设计与实现,包括渐进式加载、执行模式、扩展点以及与其他组件的集成方式。
1. 渐进式加载机制
设计理念
Skill 采用 Progressive Disclosure(渐进式展示) 模式,减少 token 消耗的同时保持 agent 对可用技能的认知。
加载策略
第一阶段:List() → 只返回 FrontMatter(元数据)
└─ name, description, context, agent, model
第二阶段:Get() → 按需获取完整 Skill
└─ Content(完整指令)+ BaseDirectory
实现位置
go
// adk/middlewares/skill/skill.go
func (s *skillTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
skills, err := s.b.List(ctx) // 仅加载元数据
// ...
}
func (s *skillTool) InvokableRun(ctx context.Context, argumentsInJSON string, ...) (string, error) {
skill, err := s.b.Get(ctx, args.Skill) // 按需加载完整内容
// ...
}
2. 文件访问机制
references/scripts 的处理方式
框架不会自动加载 references 或 scripts 文件,采用隐式按需访问策略:
| 组件 | 处理方式 |
|---|---|
| SKILL.md | 框架读取并解析 |
| FrontMatter | 框架解析(name/description/context/agent/model) |
| Content | 框架读取并返回 |
| references | 不加载,由 Agent 按需访问 |
| scripts | 不加载,由 Agent 按需访问 |
设计思路
- BaseDirectory 提供路径标识
- Skill Content 说明访问方式
- 路径转换由 Agent 负责
go
// prompt.go:144-147
userContent = `Base directory for this skill: %s
%s`
3. 云端存储支持
Backend 抽象接口
go
// adk/filesystem/backend.go
type Backend interface {
LsInfo(ctx context.Context, req *LsInfoRequest) ([]FileInfo, error)
Read(ctx context.Context, req *ReadRequest) (*FileContent, error)
GrepRaw(ctx context.Context, req *GrepRequest) ([]GrepMatch, error)
GlobInfo(ctx context.Context, req *GlobInfoRequest) ([]FileInfo, error)
Write(ctx context.Context, req *WriteRequest) error
Edit(ctx context.Context, req *EditRequest) error
}
不同 Backend 的 BaseDirectory 含义
| Backend 类型 | BaseDirectory 含义 | 示例 |
|---|---|---|
| 本地文件系统 | 本地绝对路径 | /home/user/skills/my-skill |
| 内存存储 | 虚拟路径 | /skills/my-skill |
| 云端存储 | 云端路径标识符 | s3://bucket/skills/my-skill |
云端 Skill 的实现方案
- 实现云端 Backend:读取 SKILL.md 内容
- Skill Content 说明访问方式:告知 Agent 如何获取 references/scripts
- 提供对应的访问工具:让 Agent 能通过 BaseDirectory 访问云端资源
4. 与 ReAct 模式的集成
集成方式
Skill 已经天然支持 ReAct 模式,因为 ChatModelAgent 底层就是 ReAct 实现:
go
// 创建 skill middleware
skillHandler, _ := skill.NewMiddleware(ctx, &skill.Config{
Backend: backend,
})
// 加到 ChatModelAgent 的 Handlers 中
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "my-agent",
Model: model,
Handlers: []adk.ChatModelAgentMiddleware{
skillHandler, // 自动注入到 ReAct 流程
},
})
工作原理
- BeforeAgent 注入系统提示:添加 Skills System 指导
- BeforeAgent 注入 skill 工具:添加到工具列表
- ReAct 循环中可用:Model 可以思考-调用 skill-执行任务
go
// skill.go:246-250
func (h *skillHandler) BeforeAgent(ctx context.Context, runCtx *adk.ChatModelAgentContext) (...) {
runCtx.Instruction = runCtx.Instruction + "\n" + h.instruction
runCtx.Tools = append(runCtx.Tools, h.tool)
return ctx, runCtx, nil
}
5. 工具集成
Skill 是自带的工具
无需单独实现,框架已提供完整的 skill 加载工具。
使用方式
go
// 1. 创建 Backend(指定 skill 从哪里加载)
backend, _ := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{
Backend: filesystem.NewLocalFSBackend(),
BaseDir: "/path/to/skills",
})
// 2. 创建 skill middleware(框架自动配置)
skillHandler, _ := skill.NewMiddleware(ctx, &skill.Config{
Backend: backend,
})
// 3. 配置到 Agent
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Model: model,
Handlers: []adk.ChatModelAgentMiddleware{skillHandler},
})
Function Call 流程
用户请求 "处理这个PDF"
↓
Model 思考并查看可用技能列表
↓
Model 发起 function call: {"name": "skill", "arguments": "{"skill": "pdf"}"}
↓
skill.InvokableRun() 执行
↓
返回 pdf skill 的完整指令
↓
Model 按照指令执行任务
6. 核心数据结构
skillTool
go
type skillTool struct {
b Backend // skill 数据源
toolName string // 工具名称,默认 "skill"
useChinese bool // 是否使用中文提示
// 可选组件
agentHub AgentHub // 用于 fork 模式创建子 agent
modelHub ModelHub // 用于 skill 指定模型时解析
// 扩展点
customToolDesc ToolDescriptionFunc
customToolParams func(ctx context.Context, defaults) (map[string]*schema.ParameterInfo, error)
buildContent func(ctx context.Context, skill Skill, rawArgs string) (string, error)
buildForkMessages func(ctx context.Context, in SubAgentInput) ([]adk.Message, error)
formatForkResult func(ctx context.Context, in SubAgentOutput) (string, error)
}
7. 三种执行模式
Inline 模式(默认)
- 上下文:当前 agent
- 历史消息:✅ 保留
- 执行方式:返回 skill 内容作为 tool result
- 适用场景:简单指令注入
go
// 输出示例
Launching skill: pdf
Base directory for this skill: /skills/pdf
# PDF Processing Skill
...
Fork 模式
- 上下文:新的子 agent
- 历史消息:❌ 不保留
- 执行方式:子 agent 从头开始,只有 skill 内容
- 适用场景:需要干净上下文的重任务
go
messages = []adk.Message{schema.UserMessage(skillContent)}
Fork With Context 模式
- 上下文:新的子 agent
- 历史消息:✅ 保留
- 执行方式:子 agent 获得完整历史 + skill 内容
- 适用场景:需要历史上下文的重任务
go
messages = append(history, schema.ToolMessage(skillContent, toolCallID))
8. 扩展点详解
8.1 CustomToolParams - 自定义工具参数
go
Config{
Backend: backend,
CustomToolParams: func(ctx context.Context, defaults map[string]*schema.ParameterInfo) (map[string]*schema.ParameterInfo, error) {
defaults["task"] = &schema.ParameterInfo{
Type: schema.String,
Desc: "Specific task to perform",
Required: false,
}
return defaults, nil
},
}
8.2 BuildContent - 自定义 skill 内容生成
go
BuildContent: func(ctx context.Context, skill Skill, rawArgs string) (string, error) {
var args struct {
Skill string `json:"skill"`
Task string `json:"task"`
}
json.Unmarshal([]byte(rawArgs), &args)
return fmt.Sprintf("# %s\n\nTask: %s\n\n%s", skill.Name, args.Task, skill.Content), nil
}
8.3 BuildForkMessages - 自定义子 agent 消息
go
BuildForkMessages: func(ctx context.Context, in SubAgentInput) ([]adk.Message, error) {
return []adk.Message{
schema.SystemMessage("You are a specialist for this task."),
schema.UserMessage(in.SkillContent),
}, nil
}
8.4 FormatForkResult - 自定义结果格式
go
FormatForkResult: func(ctx context.Context, in SubAgentOutput) (string, error) {
summary := summarizeResults(in.Results)
return fmt.Sprintf("✅ Task completed\n\nSummary:\n%s", summary), nil
}
9. 与其他组件的交互
Backend 接口
go
type Backend interface {
List(ctx context.Context) ([]FrontMatter, error)
Get(ctx context.Context, name string) (Skill, error)
}
ModelHub - 模型切换
go
// skill 指定模型时设置运行时变量
func (s *skillTool) setActiveModel(ctx context.Context, modelName string) {
adk.SetRunLocalValue(ctx, activeModelKey, modelName)
}
// WrapModel 读取并切换模型
func (h *skillHandler) WrapModel(ctx context.Context, m model.BaseChatModel, ...) (model.BaseChatModel, error) {
modelName, found, _ := adk.GetRunLocalValue(ctx, activeModelKey)
if found {
newModel, _ := h.tool.modelHub.Get(ctx, modelName.(string))
return newModel, nil
}
return m, nil
}
AgentHub - 子 agent 获取
go
type AgentHub interface {
Get(ctx context.Context, name string, opts *AgentHubOptions) (adk.Agent, error)
}
type AgentHubOptions struct {
Model model.ToolCallingChatModel // skill 指定的模型
}
10. Info 方法 - 生成工具描述
go
func (s *skillTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
// 1. 获取所有 skill 的元数据
skills, err := s.b.List(ctx)
// 2. 生成工具描述(列出所有可用 skill)
var fullDesc string
if s.customToolDesc != nil {
fullDesc = s.customToolDesc(ctx, skills)
} else {
desc, _ := renderToolDescription(skills)
fullDesc = toolDescriptionBase + desc
}
// 3. 构建参数 schema
oneOf, err := s.buildParamsOneOf(ctx)
return &schema.ToolInfo{
Name: s.toolName, // "skill"
Desc: fullDesc,
ParamsOneOf: oneOf,
}, nil
}
11. 核心执行流程
go
func (s *skillTool) InvokableRun(ctx context.Context, argumentsInJSON string, ...) (string, error) {
// 1. 解析参数
args := &inputArguments{} // {"skill": "pdf"}
json.Unmarshal([]byte(argumentsInJSON), args)
// 2. 获取 skill(按需加载完整内容)
skill, err := s.b.Get(ctx, args.Skill)
// 3. 根据 context 模式执行
switch skill.Context {
case ContextModeForkWithContext:
return s.runAgentMode(ctx, skill, true, argumentsInJSON)
case ContextModeFork:
return s.runAgentMode(ctx, skill, false, argumentsInJSON)
default: // inline
if skill.Model != "" {
s.setActiveModel(ctx, skill.Model)
}
return s.buildSkillResult(ctx, skill, argumentsInJSON)
}
}
总结
Skill 机制的核心设计
- 渐进式加载:List 只返回元数据,Get 按需加载完整内容
- 三种执行模式:inline/fork/fork_with_context,满足不同场景
- 丰富的扩展点:支持自定义参数、内容、消息、结果格式
- 组件解耦:Backend/AgentHub/ModelHub 都可替换
- 模型切换:通过 run local value 实现运行时模型切换
- 开箱即用:框架自带完整实现,无需单独开发
- 天然支持 ReAct:通过 ChatModelAgentMiddleware 集成
适用场景
- Inline:简单的指令注入,增强 agent 能力
- Fork:需要干净上下文的复杂任务
- Fork With Context:需要历史上下文的复杂任务
- 云端存储:通过实现自定义 Backend 支持