核心概念

工具系统

理解工具注册、执行和自定义开发

工具系统

aster的工具系统为Agent提供了与外部世界交互的能力。从文件操作到网络请求,工具让Agent能够执行实际任务。

🛠️ 工具概念

什么是工具?

工具(Tool)是Agent可以调用的函数,用于执行特定任务:

Agent思考 → 决定使用工具 → 调用工具 → 获取结果 → 继续思考

工具的作用

  • 扩展能力:让Agent能做LLM本身做不到的事
  • 实时数据:获取最新信息(搜索、API调用)
  • 副作用操作:修改文件、执行命令
  • 结构化输出:返回格式化数据

📋 Tool接口

接口定义

type Tool interface {
    // 工具名称(唯一标识)
    Name() string

    // 工具描述(告诉LLM这个工具的用途)
    Description() string

    // 输入参数schema(JSON Schema格式)
    InputSchema() map[string]interface{}

    // 执行工具
    Execute(ctx context.Context, input map[string]interface{}) (interface{}, error)
}

简单示例

type CalculatorTool struct{}

func (t *CalculatorTool) Name() string {
    return "calculator"
}

func (t *CalculatorTool) Description() string {
    return "执行数学计算,支持加减乘除运算"
}

func (t *CalculatorTool) InputSchema() map[string]interface{} {
    return map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "expression": map[string]interface{}{
                "type":        "string",
                "description": "数学表达式,例如: 2+2, 10*5",
            },
        },
        "required": []string{"expression"},
    }
}

func (t *CalculatorTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    expr := input["expression"].(string)

    // 执行计算(实际应使用安全的表达式解析器)
    result, err := evaluate(expr)
    if err != nil {
        return map[string]interface{}{
            "ok":    false,
            "error": err.Error(),
        }, nil  // 注意:返回nil error
    }

    return map[string]interface{}{
        "ok":     true,
        "result": result,
    }, nil
}

📚 内置工具

aster提供丰富的内置工具,覆盖常见使用场景。

文件系统工具

Read

读取文件内容(支持分页):

// 工具调用
{
    "name": "Read",
    "input": {
        "path": "/README.md",
        "offset": 0,      // 起始行号(可选)
        "limit": 100      // 读取行数(可选)
    }
}

// 返回结果
{
    "ok": true,
    "content": "文件内容...",
    "lines": 100,
    "total_lines": 250,
    "has_more": true
}

Write

写入文件:

{
    "name": "Write",
    "input": {
        "path": "/output.txt",
        "content": "Hello World"
    }
}

Edit

精确编辑(字符串替换):

{
    "name": "Edit",
    "input": {
        "path": "/config.json",
        "old_string": "\"debug\": false",
        "new_string": "\"debug\": true",
        "replace_all": false
    }
}

Ls

列出目录:

{
    "name": "Ls",
    "input": {
        "path": "/src"
    }
}

// 返回
{
    "ok": true,
    "entries": [
        {"name": "main.go", "size": 1024, "is_dir": false},
        {"name": "utils", "size": 0, "is_dir": true}
    ]
}

Glob

Glob模式匹配:

{
    "name": "Glob",
    "input": {
        "pattern": "**/*.go",
        "path": "/src"
    }
}

Grep

正则搜索:

{
    "name": "Grep",
    "input": {
        "pattern": "func.*Error",
        "path": "/src",
        "glob": "*.go"
    }
}

// 返回
{
    "ok": true,
    "matches": [
        {
            "file": "/src/main.go",
            "line": 42,
            "content": "func handleError(err error) {"
        }
    ]
}

命令执行工具

Bash

执行Bash命令:

{
    "name": "Bash",
    "input": {
        "command": "go test ./...",
        "timeout": 30000  // 毫秒
    }
}

// 返回
{
    "ok": true,
    "stdout": "PASS\nok\t...",
    "stderr": "",
    "exit_code": 0
}

网络工具

http_fetch

HTTP请求:

{
    "name": "http_fetch",
    "input": {
        "url": "https://api.example.com/data",
        "method": "GET",
        "headers": {"Authorization": "Bearer token"},
        "body": null
    }
}

WebSearch

网络搜索(Tavily API):

{
    "name": "WebSearch",
    "input": {
        "query": "Go语言性能优化",
        "search_type": "general",  // general/news/finance
        "max_results": 5
    }
}

任务管理工具

todo_list

列出待办事项:

{
    "name": "todo_list",
    "input": {}
}

todo_add

添加待办:

{
    "name": "todo_add",
    "input": {
        "title": "实现用户认证",
        "description": "添加JWT认证",
        "priority": "high"
    }
}

todo_update

更新待办状态:

{
    "name": "todo_update",
    "input": {
        "id": "todo-123",
        "status": "completed"
    }
}

🔧 工具注册

创建注册表

import (
    "github.com/astercloud/aster/pkg/tools"
    "github.com/astercloud/aster/pkg/tools/builtin"
)

// 创建工具注册表
registry := tools.NewRegistry()

注册内置工具

// 方式1:注册所有内置工具
builtin.RegisterAll(registry)

// 方式2:选择性注册
registry.Register(builtin.NewFileSystemTool())
registry.Register(builtin.NewBashTool())
registry.Register(builtin.NewHTTPTool())

注册自定义工具

// 注册自定义工具
registry.Register(&CalculatorTool{})
registry.Register(&WeatherTool{})

在Agent中使用

ag, err := agent.Create(ctx, &types.AgentConfig{
    TemplateID: "assistant",
    // 指定允许使用的工具
    Tools: []interface{}{
        "Read",     // 工具名称
        "Write",
        "Bash",
        &MyTool{},     // 或直接传入工具实例
    },
}, &agent.Dependencies{
    ToolRegistry: registry,
    // ...其他依赖
})

🎨 创建自定义工具

基础工具

package mytools

import (
    "context"
    "encoding/json"
    "net/http"
)

type WeatherTool struct{}

func (t *WeatherTool) Name() string {
    return "get_weather"
}

func (t *WeatherTool) Description() string {
    return "获取指定城市的天气信息"
}

func (t *WeatherTool) InputSchema() map[string]interface{} {
    return map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "city": map[string]interface{}{
                "type":        "string",
                "description": "城市名称,例如: Beijing, Shanghai",
            },
        },
        "required": []string{"city"},
    }
}

func (t *WeatherTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    city := input["city"].(string)

    // 调用天气API
    weather, err := t.fetchWeather(ctx, city)
    if err != nil {
        return map[string]interface{}{
            "ok":    false,
            "error": err.Error(),
        }, nil
    }

    return map[string]interface{}{
        "ok":          true,
        "city":        city,
        "temperature": weather.Temp,
        "condition":   weather.Condition,
        "humidity":    weather.Humidity,
    }, nil
}

func (t *WeatherTool) fetchWeather(ctx context.Context, city string) (*Weather, error) {
    // 实际API调用逻辑
    url := fmt.Sprintf("https://api.weather.com/v1/current?city=%s", city)
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var weather Weather
    json.NewDecoder(resp.Body).Decode(&weather)
    return &weather, nil
}

带状态的工具

type CounterTool struct {
    count int
    mu    sync.Mutex
}

func (t *CounterTool) Name() string {
    return "counter"
}

func (t *CounterTool) Description() string {
    return "计数器工具,可以增加或查询计数"
}

func (t *CounterTool) InputSchema() map[string]interface{} {
    return map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "action": map[string]interface{}{
                "type": "string",
                "enum": []string{"increment", "get", "reset"},
            },
        },
        "required": []string{"action"},
    }
}

func (t *CounterTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    action := input["action"].(string)

    t.mu.Lock()
    defer t.mu.Unlock()

    switch action {
    case "increment":
        t.count++
        return map[string]interface{}{
            "ok":    true,
            "count": t.count,
        }, nil

    case "get":
        return map[string]interface{}{
            "ok":    true,
            "count": t.count,
        }, nil

    case "reset":
        t.count = 0
        return map[string]interface{}{
            "ok":      true,
            "message": "计数器已重置",
        }, nil

    default:
        return map[string]interface{}{
            "ok":    false,
            "error": "未知操作",
        }, nil
    }
}

异步工具

type AsyncTool struct{}

func (t *AsyncTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    // 创建后台任务
    taskID := uuid.New().String()

    go func() {
        // 长时间运行的任务
        time.Sleep(10 * time.Second)
        result := processLongTask(input)

        // 将结果存储到某处
        store.SaveResult(taskID, result)
    }()

    return map[string]interface{}{
        "ok":      true,
        "task_id": taskID,
        "status":  "processing",
        "message": "任务已启动,请使用task_id查询结果",
    }, nil
}

🔄 工具执行流程

完整流程

1. LLM决定使用工具
     │
     ▼
2. 生成tool_use块
     │  {
     │    "type": "tool_use",
     │    "id": "toolu_123",
     │    "name": "Read",
     │    "input": {"path": "/file.txt"}
     │  }
     ▼
3. Agent提取工具调用
     │
     ▼
4. 查找工具(Registry.GetTool)
     │
     ▼
5. 验证输入(InputSchema)
     │
     ▼
6. 通过中间件栈(WrapToolCall)
     │
     ▼
7. 执行工具(tool.Execute)
     │
     ▼
8. 构造tool_result消息
     │  {
     │    "type": "tool_result",
     │    "tool_use_id": "toolu_123",
     │    "content": "{\"ok\": true, ...}"
     │  }
     ▼
9. 继续对话

错误处理

重要:工具应该返回结构化错误,而不是Go error:

// ✅ 推荐:返回结构化错误
func (t *MyTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    result, err := doSomething()
    if err != nil {
        return map[string]interface{}{
            "ok":    false,
            "error": err.Error(),
            "suggestions": []string{
                "检查输入参数",
                "确认权限设置",
            },
        }, nil  // 返回nil error!
    }

    return map[string]interface{}{
        "ok":     true,
        "result": result,
    }, nil
}

// ❌ 不推荐:直接返回error
func (t *MyTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    result, err := doSomething()
    if err != nil {
        return nil, err  // LLM看不到错误信息
    }
    return result, nil
}

原因:LLM需要看到错误信息才能尝试恢复或给出建议。

🎯 高级模式

工具组合

type CompositeTool struct {
    tools []Tool
}

func (t *CompositeTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    results := make([]interface{}, 0)

    // 依次执行多个工具
    for _, tool := range t.tools {
        result, err := tool.Execute(ctx, input)
        if err != nil {
            return nil, err
        }
        results = append(results, result)
    }

    return map[string]interface{}{
        "ok":      true,
        "results": results,
    }, nil
}

工具代理

type ProxyTool struct {
    target Tool
}

func (t *ProxyTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    // 前置处理
    log.Printf("执行工具: %s", t.target.Name())
    start := time.Now()

    // 执行目标工具
    result, err := t.target.Execute(ctx, input)

    // 后置处理
    duration := time.Since(start)
    log.Printf("工具完成: %s, 耗时: %v", t.target.Name(), duration)

    return result, err
}

条件工具

type ConditionalTool struct {
    condition func(map[string]interface{}) bool
    trueTool  Tool
    falseTool Tool
}

func (t *ConditionalTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    if t.condition(input) {
        return t.trueTool.Execute(ctx, input)
    }
    return t.falseTool.Execute(ctx, input)
}

🎯 最佳实践

1. 清晰的描述

// ✅ 推荐:具体的描述
func (t *Tool) Description() string {
    return "从指定URL下载文件到本地目录。支持HTTP/HTTPS,可配置超时。返回下载的文件路径和大小。"
}

// ❌ 不推荐:模糊的描述
func (t *Tool) Description() string {
    return "下载文件"
}

2. 完整的Schema

// ✅ 推荐:详细的schema
func (t *Tool) InputSchema() map[string]interface{} {
    return map[string]interface{}{
        "type": "object",
        "properties": map[string]interface{}{
            "url": map[string]interface{}{
                "type":        "string",
                "description": "要下载的文件URL",
                "pattern":     "^https?://",
            },
            "output_path": map[string]interface{}{
                "type":        "string",
                "description": "保存文件的本地路径",
            },
            "timeout": map[string]interface{}{
                "type":        "integer",
                "description": "超时时间(秒),默认60秒",
                "default":     60,
                "minimum":     1,
                "maximum":     300,
            },
        },
        "required": []string{"url"},
    }
}

3. 结构化输出

// ✅ 推荐:结构化返回
return map[string]interface{}{
    "ok":        true,
    "file_path": "/downloads/file.pdf",
    "file_size": 1048576,
    "mime_type": "application/pdf",
    "duration":  "2.5s",
}, nil

// ❌ 不推荐:字符串返回
return "文件已下载到 /downloads/file.pdf", nil

4. 幂等性

// 工具应该尽可能幂等
func (t *CreateFileTool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    path := input["path"].(string)

    // 检查文件是否已存在
    if fileExists(path) {
        return map[string]interface{}{
            "ok":      true,
            "existed": true,
            "message": "文件已存在",
        }, nil
    }

    // 创建文件
    return createFile(path)
}

5. 超时控制

func (t *Tool) Execute(ctx context.Context, input map[string]interface{}) (interface{}, error) {
    // 使用context控制超时
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // 可取消的操作
    result := make(chan interface{}, 1)
    go func() {
        result <- doWork()
    }()

    select {
    case r := <-result:
        return r, nil
    case <-ctx.Done():
        return map[string]interface{}{
            "ok":    false,
            "error": "操作超时",
        }, nil
    }
}

📚 下一步

🔗 相关资源