源码分析Eino框架工具调用--创建篇
Eino框架中通过tool node这个官方的组件给模型加上了手脚,工具有多种创建方式,本文不讨论如何创建tool而是聚焦于调用工具过程的底层实现,关于工具的其他内容可以参考这里创建tool
调用一个工具需要这些东西:名称,描述,参数说明以及实现(本地or mcp)
先看tool node的创建,总的来说创建节点的过程分为以下几步
- info方法检测
- invoke && stream方法实现,二选一即可
- 打包原信息,和运行时有关
- 打包具体的实现了runnable这个interface的struct,至此通过下面的函数实现了所有工具以来信息的
// NewToolNode creates a new ToolsNode.
// e.g.
//
// conf := &ToolsNodeConfig{
// Tools: []tool.BaseTool{invokableTool1, streamableTool2},
// }
// toolsNode, err := NewToolNode(ctx, conf)
func NewToolNode(ctx context.Context, conf *ToolsNodeConfig) (*ToolsNode, error) {tuple, err := convTools(ctx, conf.Tools)if err != nil {return nil, err}return &ToolsNode{tuple: tuple,unknownToolHandler: conf.UnknownToolsHandler,executeSequentially: conf.ExecuteSequentially,toolArgumentsHandler: conf.ToolArgumentsHandler,}, nil
}
最后的ToolNode结构其实是这样的
type ToolsNode struct {tuple *toolsTuple // 核心结构unknownToolHandler func(ctx context.Context, name, input string) (string, error) // 当模型调用了不存在的func时触发这个handler,若没有指定会直接报错executeSequentially bool // 多个工具的调用顺序是穿行还是并行toolArgumentsHandler func(ctx context.Context, name, input string) (string, error) // 处理入参的handler
}
/*
这个是toolNode的核心结构,可以看到indexes是map其他的都是切片,这点是为了根据被调用工具的名字快速找到index后去meta和rps中找到需要的其他数据
meta存储一些元信息
rps是工具执行的关键,实现了eino框架的invoke或者是stream
*/
type toolsTuple struct {indexes map[string]int meta []*executorMetarps []*runnablePacker[string, string, tool.Option]
}
我们这里重点关注一下tuple的创建过程,以invoke调用进行说明,创建tuple的源码如下
func convTools(ctx context.Context, tools []tool.BaseTool) (*toolsTuple, error) {ret := &toolsTuple{indexes: make(map[string]int),meta: make([]*executorMeta, len(tools)),rps: make([]*runnablePacker[string, string, tool.Option], len(tools)),}for idx, bt := range tools {// 检测是否实现了info方法tl, err := bt.Info(ctx)if err != nil {return nil, fmt.Errorf("(NewToolNode) failed to get tool info at idx= %d: %w", idx, err)}toolName := tl.Namevar (st tool.StreamableToolit tool.InvokableToolinvokable func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)streamable func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error)ok boolmeta *executorMeta)if st, ok = bt.(tool.StreamableTool); ok {streamable = st.StreamableRun}if it, ok = bt.(tool.InvokableTool); ok {invokable = it.InvokableRun}// 工具只有invoke和stream两种调用方式,如果这两个方法都没实现会直接error,同时进行了流的转换,因为eino框架默认支持了流的拼接if st == nil && it == nil {return nil, fmt.Errorf("tool %s is not invokable or streamable", toolName)}// 设置上面所说的meta信息if st != nil {meta = parseExecutorInfoFromComponent(components.ComponentOfTool, st)} else {meta = parseExecutorInfoFromComponent(components.ComponentOfTool, it)}ret.indexes[toolName] = idxret.meta[idx] = meta// 这个packer通过源码可以看到是包装出一个struct实现了runnable这个interface,也就是说我们最终调用工具的时候,其实是通过这个interface的具体实现来的ret.rps[idx] = newRunnablePacker(invokable, streamable,nil, nil, !meta.isComponentCallbackEnabled)}return ret, nil
}func parseExecutorInfoFromComponent(c component, executor any) *executorMeta {componentImplType, ok := components.GetType(executor)if !ok {componentImplType = generic.ParseTypeName(reflect.ValueOf(executor))}return &executorMeta{component: c,isComponentCallbackEnabled: components.IsCallbacksEnabled(executor),componentImplType: componentImplType,}
}