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你学到了什么
- Eino Tool 接口:
Info()告诉 LLM 工具是干什么的,InvokableRun()执行工具。 - Go 的类型安全:
GetOrderTool、GetDeliveryTimeTool各自有独立的 Params/Result struct。编译期就确定类型。 - 工具注册:
map[string]tool.InvokableTool管理多个工具,按名称查找。
明天的预告
Agent 能干活了,但它记不住事。用户问"我的订单什么时候到",Agent 回答了。下一句用户问"那能加急吗"——Agent 不知道"那"指的是什么。明天我们给 Agent 装上 Eino Memory。
Day 4 完成。你的 Agent 现在能查订单、查配送时间了。

