Skip to content

Day 4:加工具——让Agent能干活

昨天的问题

Day 3 的 Agent 会查文档了,但它只能"说"不能"做"。用户问"帮我查一下订单 #12345 的状态",它只能从文档里找一般性回答。

今天做什么

给 Agent 装上 Eino Tool——它能真正调用函数、查数据库、执行操作。

代码

go
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/cloudwego/eino/components/tool"
    "github.com/cloudwego/eino/schema"
)

// ===== 模拟订单数据库 =====
var orderDB = map[string]Order{
    "12345": {ID: "12345", Status: "已发货", Courier: "顺丰", ETA: "2025-03-20"},
    "12346": {ID: "12346", Status: "处理中", Courier: "", ETA: "预计3-5天"},
}

type Order struct {
    ID      string
    Status  string
    Courier string
    ETA     string
}

// ===== 工具定义 =====

// 工具1:查订单状态
type GetOrderTool struct{}

func (t *GetOrderTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
    return &schema.ToolInfo{
        Name: "get_order_status",
        Desc: "根据订单号查询订单状态和配送信息",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "order_id": {Type: schema.String, Desc: "订单号", Required: true},
        }),
    }, nil
}

func (t *GetOrderTool) InvokableRun(ctx context.Context,
    params *OrderParams) (*OrderResult, error) {
    order, ok := orderDB[params.OrderID]
    if !ok {
        return &OrderResult{Found: false,
            Message: fmt.Sprintf("未找到订单 %s", params.OrderID)}, nil
    }
    return &OrderResult{
        Found:   true,
        Status:  order.Status,
        Courier: order.Courier,
        ETA:     order.ETA,
        Message: fmt.Sprintf("订单 %s 状态:%s,预计 %s 送达", order.ID, order.Status, order.ETA),
    }, nil
}

type OrderParams struct {
    OrderID string `json:"order_id"`
}

type OrderResult struct {
    Found   bool
    Status  string
    Courier string
    ETA     string
    Message string
}

// 工具2:查配送时间
type GetDeliveryTimeTool struct{}

func (t *GetDeliveryTimeTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
    return &schema.ToolInfo{
        Name: "get_delivery_time",
        Desc: "查询某地区的预计配送时间",
        ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
            "city": {Type: schema.String, Desc: "城市名", Required: true},
        }),
    }, nil
}

func (t *GetDeliveryTimeTool) InvokableRun(ctx context.Context,
    params *DeliveryParams) (*DeliveryResult, error) {
    deliveryMap := map[string]string{
        "北京": "1-2天", "上海": "1-2天", "深圳": "1-2天",
        "成都": "2-3天", "拉萨": "5-7天", "乌鲁木齐": "5-7天",
    }
    days := deliveryMap[params.City]
    if days == "" {
        days = "3-5天"
    }
    return &DeliveryResult{Days: days,
        Message: fmt.Sprintf("%s地区预计 %s 送达", params.City, days)}, nil
}

type DeliveryParams struct {
    City string `json:"city"`
}

type DeliveryResult struct {
    Days    string
    Message string
}

// ===== 带工具的 Agent =====
func (a *CustomerServiceAgent) AnswerWithTools(ctx context.Context,
    question string) (string, error) {

    tools := map[string]tool.InvokableTool{
        "get_order_status":   &GetOrderTool{},
        "get_delivery_time":  &GetDeliveryTimeTool{},
    }

    // 第一步:让 LLM 判断需要哪个工具
    toolName, toolParams := a.decideTool(ctx, question)

    // 第二步:如果需要工具,执行它
    if t, ok := tools[toolName]; ok {
        result, err := t.InvokableRun(ctx, toolParams)
        if err != nil {
            return "", err
        }

        // 第三步:把工具结果和用户问题一起发给 LLM
        input := []*schema.Message{
            schema.SystemMessage(systemPrompt +
                fmt.Sprintf("\n工具查询结果:%v", result)),
            schema.UserMessage(question),
        }
        response, err := a.model.Generate(ctx, input)
        if err != nil {
            return "", err
        }
        return response.Content, nil
    }

    // 不需要工具,直接回答
    return a.Answer(ctx, question)
}

func (a *CustomerServiceAgent) decideTool(ctx context.Context,
    question string) (string, interface{}) {
    // 简化版:关键词判断。生产中由 LLM 的 Function Calling 决定
    if strings.Contains(question, "订单") {
        return "get_order_status", &OrderParams{OrderID: extractOrderID(question)}
    }
    if strings.Contains(question, "几天") || strings.Contains(question, "配送") {
        return "get_delivery_time", &DeliveryParams{City: "北京"} // 简化
    }
    return "", nil
}

运行

bash
go run main.go

你学到了什么

  1. Eino Tool 接口Info() 告诉 LLM 工具是干什么的,InvokableRun() 执行工具。
  2. Go 的类型安全GetOrderToolGetDeliveryTimeTool 各自有独立的 Params/Result struct。编译期就确定类型。
  3. 工具注册map[string]tool.InvokableTool 管理多个工具,按名称查找。

明天的预告

Agent 能干活了,但它记不住事。用户问"我的订单什么时候到",Agent 回答了。下一句用户问"那能加急吗"——Agent 不知道"那"指的是什么。明天我们给 Agent 装上 Eino Memory


Day 4 完成。你的 Agent 现在能查订单、查配送时间了。