一. 创建 Tools
定义 tool 时要完成名称、描述定义,输出结果处理等流程
通过支持 function call 的大模型来进行意图识别,返回要调用的 tools
assistant : 用户需要搜索明天杭州的天气,调用 exa_search 函数获取相关信息。
{
"role": "assistant",
"content": "用户需要搜索明天杭州的天气,调用 exa_search 函数获取相关信息。",
"tool_calls": [
{
"index": 0,
"id": "call_0mjxaxv4x0smnh3tn4hi1oa2",
"type": "function",
"function": {
"name": "exa_search",
"arguments": {
"query": "明天杭州的天气"
}
}
}
],
"response_meta": {
"finish_reason": "tool_calls",
"usage": {
"prompt_tokens": 374,
"completion_tokens": 39,
"total_tokens": 413
}
},
"extra": {
"ark-request-id": "0217400164055559141e38cd6c80180b3cca79c4cbce58072cbb7"
}
}
1. 根据定义工具信息和处理函数
package agent_first
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/schema"
)
// 参数结构体
type TodoAddParams struct {
Content string `json:"content"`
StartAt *int64 `json:"started_at,omitempty"` // 开始时间
Deadline *int64 `json:"deadline,omitempty"`
}
// 处理函数
func AddTodoFunc(_ context.Context, params *TodoAddParams) (string, error) {
// Mock处理逻辑
return `{"msg": "add todo success"}`, nil
}
func getAddTodoTool() tool.InvokableTool {
// 工具信息
info := &schema.ToolInfo{
Name: "add_todo",
Desc: "Add a todo item",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"content": {
Desc: "The content of the todo item",
Type: schema.String,
Required: true,
},
"started_at": {
Desc: "The started time of the todo item, in unix timestamp",
Type: schema.Integer,
},
"deadline": {
Desc: "The deadline of the todo item, in unix timestamp",
Type: schema.Integer,
},
}),
}
// 使用NewTool创建工具
return utils.NewTool(info, AddTodoFunc)
}
2. 使用InferTool来映射结构体参数
package agent_first
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
)
// 参数结构体
type TodoUpdateParams struct {
ID string `json:"id" jsonschema:"description=id of the todo"`
Content *string `json:"content,omitempty" jsonschema:"description=content of the todo"`
StartedAt *int64 `json:"started_at,omitempty" jsonschema:"description=start time in unix timestamp"`
Deadline *int64 `json:"deadline,omitempty" jsonschema:"description=deadline of the todo in unix timestamp"`
Done *bool `json:"done,omitempty" jsonschema:"description=done status"`
}
// 处理函数
func UpdateTodoFunc(_ context.Context, params *TodoUpdateParams) (string, error) {
// Mock处理逻辑
return `{"msg": "update todo success"}`, nil
}
func getUpdateTodoTool() tool.InvokableTool {
// 使用 InferTool 创建工具
updateTool, err := utils.InferTool(
"update_todo", // tool name
"Update a todo item, eg: content,deadline...", // tool description
UpdateTodoFunc)
if err != nil {
panic(err)
}
return updateTool
}
3. 通过实现 Tool 接口
package agent_first
import (
"context"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/schema"
)
type ListTodoTool struct{}
func (lt *ListTodoTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
return &schema.ToolInfo{
Name: "list_todo",
Desc: "List all todo items",
ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
"finished": {
Desc: "filter todo items if finished",
Type: schema.Boolean,
Required: false,
},
}),
}, nil
}
func (lt *ListTodoTool) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
// Mock调用逻辑
return `{"todos": [{"id": "1", "content": "在2024年12月10日之前完成Eino项目演示文稿的准备工作", "started_at": 1717401600, "deadline": 1717488000, "done": false}]}`, nil
}
func getListTodoTool() tool.InvokableTool {
return &ListTodoTool{}
}
4. 使用官方的 tool
5. 自定义 Exa 搜索
package agent_first
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/bytedance/sonic"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"eino-demo/env"
)
type ExaConfig struct {
APIKey string `json:"api_key"`
}
const (
ExaSearchBaseURL = "https://api.exa.ai/search"
)
/*
curl -X POST https://api.exa.ai/search \
--header "content-type: application/json" \
--header "Authorization: Bearer " \
--data '
{
"query": "startup that has developed a breakthrough AI model",
"category": "company",
"type": "keyword",
"numResults": 3,
"contents": {
"text": {
"maxCharacters": 500
}
}
}'
*/
type ExaSearchParam struct {
Query string `json:"query" jsonschema:"description=search query string"`
}
type ExaSearchRequest struct {
Query string `json:"query" jsonschema:"description=search query string"`
Category string `json:"category" jsonschema:"description=search category"`
Type string `json:"type" jsonschema:"description=search type"`
NumResults int `json:"numResults" jsonschema:"description=number of results returned"`
Contents struct {
Text struct {
MaxCharacters int `json:"maxCharacters" jsonschema:"description=text maximum character count"`
} `json:"text" jsonschema:"description=text content"`
} `json:"contents" jsonschema:"description=search content"`
}
type ExaSearchResponse struct {
AutopromptString string `json:"autopromptString"`
Results []SearchResultItem `json:"results"`
}
type SearchResultItem struct {
// 根据 Exa API 的返回结构定义字段
Title string `json:"title"`
Text string `json:"text"`
Url string `json:"url"`
}
func getExaTool() tool.InvokableTool {
conf := &ExaConfig{
APIKey: env.ExaSearchAPIKey,
}
toolName := "exa_search"
toolDesc := "custom search json api of exa search engine"
ex := &exaTool{conf: conf}
tl, err := utils.InferTool(toolName, toolDesc,
ex.Search, utils.WithMarshalOutput(ex.marshalOutput))
if err != nil {
return nil
}
return tl
}
type exaTool struct {
conf *ExaConfig
}
func (t *exaTool) Search(ctx context.Context, query *ExaSearchParam) (*ExaSearchResponse, error) {
log.Println("search query: ", query)
reqBody := ExaSearchRequest{
Query: query.Query,
Category: "company",
Type: "keyword",
NumResults: 3,
}
reqBody.Contents.Text.MaxCharacters = 500
body, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", ExaSearchBaseURL, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+t.conf.APIKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to search: %s", resp.Status)
}
var searchResponse ExaSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil {
return nil, err
}
return &searchResponse, nil
}
func (t *exaTool) marshalOutput(_ context.Context, output any) (string, error) {
// 将输出转换为 ExaSearchResponse 类型
exaResponse, ok := output.(*ExaSearchResponse)
if !ok {
return "", fmt.Errorf("unexpected output type, expect %T but given %T", exaResponse, output)
}
// 创建简化的搜索结果
simpleItems := make([]*SimplifiedSearchItem, 0, len(exaResponse.Results))
for _, item := range exaResponse.Results {
ssi := &SimplifiedSearchItem{
Url: item.Url,
Title: item.Title,
Text: item.Text,
}
simpleItems = append(simpleItems, ssi)
}
// 创建最终的搜索结果
sr := SearchResult{
Query: exaResponse.AutopromptString,
Items: simpleItems,
}
// 将结果序列化为 JSON 字符串
return sonic.MarshalString(sr)
}
type SearchResult struct {
Query string `json:"query,omitempty"`
Items []*SimplifiedSearchItem `json:"items"`
}
type SimplifiedSearchItem struct {
Url string `json:"url"`
Title string `json:"title,omitempty"`
Text string `json:"text,omitempty"`
}
二. 创建 Model
// 创建模型
chatModel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{
BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
APIKey: env.HuoshanAPIKey,
Model: "doubao-1-5-pro-32k-250115",
})
if err != nil {
log.Fatal(err)
}
三. 创建 Chain
1. 注册tools
todoTools := []tool.BaseTool{
getAddTodoTool(), // 添加列表工具
getUpdateTodoTool(), // 更新列表工具
getListTodoTool(), // 查询列表工具
getExaTool(), // 搜索引擎工具
}
2. 模型绑定tools
// 获取工具信息并绑定到 ChatModel
toolInfos := make([]*schema.ToolInfo, 0, len(todoTools))
for _, tool := range todoTools {
info, err := tool.Info(ctx)
if err != nil {
log.Fatal(err)
}
toolInfos = append(toolInfos, info)
}
err = chatModel.BindTools(toolInfos)
if err != nil {
log.Fatal(err)
}
3. 创建tools 节点
// 创建tools节点
conf := &compose.ToolsNodeConfig{
// 工具可以是 InvokableTool 或 StreamableTool
Tools: todoTools,
}
toolsNode, err := compose.NewToolNode(ctx, conf)
if err != nil {
log.Fatal(err)
}
4. 构建 chain
// 构建完整的处理链
chain := compose.NewChain[[]*schema.Message, *schema.Message]()
chain.
AppendChatModel(chatModel, compose.WithNodeName("chat_model")).
AppendToolsNode(toolsNode, compose.WithNodeName("tools")).
AppendLambda(compose.InvokableLambda(func(ctx context.Context, input []*schema.Message) ([]*schema.Message, error) {
return []*schema.Message{
{
Role: schema.Assistant,
Content: "Only the weather for tomorrow, No superfluous content, format [date, city, weather, temperature]:" + input[0].Content,
},
}, nil
})).
AppendChatModel(chatModel, compose.WithNodeName("chat_model"))
5. 编译运行
// 编译chain
agent, err := chain.Compile(ctx)
if err != nil {
log.Fatal(err)
}
// 运行chain
resp, err := agent.Invoke(ctx, []*schema.Message{
{
Role: schema.User,
Content: "搜索一下明天杭州的天气", // , 然后帮我添加一个todo待办:吃中午饭红烧茄子
},
})
if err != nil {
log.Fatal(err)
}
// 输出结果
fmt.Println(resp.Content)
// for _, msg := range resp {
// fmt.Println(msg.Content)
// }
四. 效果
五. 链路追踪
六. 注意事项
1. 节点之间的参数传递规则
模型和 tools 有自己指定的传参类型,参数转换等操作可以通过加入AppendLambda调整
AppendLambda(compose.InvokableLambda(func(ctx context.Context, input []*schema.Message) ([]*schema.Message, error) {
return []*schema.Message{
{
Role: schema.Assistant,
Content: "Only the weather for tomorrow, No superfluous content, format [date, city, weather, temperature]:" + input[0].Content,
},
}, nil
})).
2. 如何确保意图识别可以识别多个tools调用
todo