☰
第3讲:工具系统设计——注册、校验、沙箱、限流
2026/10/16 3:07:22 网站建设 项目流程

一、为什么工具系统是 Agent 的核心

Agent 的能力边界 = 它能调用的工具集合。

一个只有“搜索”工具的 Agent 只能查资料;加上“代码执行”就能写代码跑测试;再加上“数据库查询”就能做数据分析。工具系统的设计质量,直接决定了 Agent 的上限和安全性。

┌─────────────────────────────────────────────────────────┐ │ 工具系统架构 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 注册中心 │───▶│ 校验器 │───▶│ 执行器 │ │ │ │ (Registry)│ │(Validator)│ │(Executor)│ │ │ └──────────┘ └──────────┘ └────┬─────┘ │ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 限流器 │ │ 追踪器 │ │ 沙箱 │ │ │ │ (Limiter)│ │ (Tracer) │ │ (Sandbox) │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 工具注册表 (Registry Map) │ │ │ │ search │ calculator │ db_query │ code_run │ ... │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

二、工具注册中心

2.1 数据结构

// tools/registry.go package tools import ( "encoding/json" "fmt" "sync" "time" ) // ToolMeta 工具元数据 type ToolMeta struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description"` Category string `json:"category"` // internal_api, external_api, code_exec, db_query Tags []string `json:"tags"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Owner string `json:"owner"` // 负责人 RiskLevel string `json:"risk_level"` // low / medium / high / critical } // ToolSpec 工具规格定义 type ToolSpec struct { Meta ToolMeta `json:"meta"` Parameters json.RawMessage `json:"parameters"` // JSON Schema Returns json.RawMessage `json:"returns"` // JSON Schema Examples []ToolExample `json:"examples"` Timeout time.Duration `json:"timeout"` // 单次调用超时 RateLimit RateLimitConfig `json:"rate_limit"` } type ToolExample struct { Input json.RawMessage `json:"input"` Output string `json:"output"` Desc string `json:"desc"` } type RateLimitConfig struct { MaxCallsPerMinute int `json:"max_calls_per_minute"` MaxCallsPerHour int `json:"max_calls_per_hour"` MaxCallsPerDay int `json:"max_calls_per_day"` } // ToolInstance 工具运行时实例 type ToolInstance struct { Spec ToolSpec Execute func(ctx context.Context, input json.RawMessage) (string, error) mu sync.RWMutex callLog []CallRecord } type CallRecord struct { Timestamp time.Time Input json.RawMessage Output string Duration time.Duration Error string TraceID string } // Registry 工具注册中心 type Registry struct { mu sync.RWMutex tools map[string]*ToolInstance } func NewRegistry() *Registry { return &Registry{ tools: make(map[string]*ToolInstance), } } // Register 注册工具 func (r *Registry) Register(spec ToolSpec, executor func(ctx context.Context, input json.RawMessage) (string, error)) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.tools[spec.Meta.Name]; exists { return fmt.Errorf("tool %s already registered", spec.Meta.Name) } // 参数校验:必须有 JSON Schema if len(spec.Parameters) == 0 { return fmt.Errorf("tool %s must have parameters schema", spec.Meta.Name) } // 默认超时 if spec.Timeout == 0 { spec.Timeout = 30 * time.Second } r.tools[spec.Meta.Name] = &ToolInstance{ Spec: spec, Execute: executor, callLog: make([]CallRecord, 0), } return nil } // Unregister 注销工具 func (r *Registry) Unregister(name string) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.tools[name]; !exists { return fmt.Errorf("tool %s not found", name) } delete(r.tools, name) return nil } // Get 获取工具 func (r *Registry) Get(name string) (*ToolInstance, error) { r.mu.RLock() defer r.mu.RUnlock() tool, exists := r.tools[name] if !exists { return nil, fmt.Errorf("tool %s not found", name) } return tool, nil } // List 列出所有工具 func (r *Registry) List() []ToolSpec { r.mu.RLock() defer r.mu.RUnlock() specs := make([]ToolSpec, 0, len(r.tools)) for _, tool := range r.tools { specs = append(specs, tool.Spec) } return specs } // ListByCategory 按分类列出 func (r *Registry) ListByCategory(category string) []ToolSpec { r.mu.RLock() defer r.mu.RUnlock() var specs []ToolSpec for _, tool := range r.tools { if tool.Spec.Meta.Category == category { specs = append(specs, tool.Spec) } } return specs }

2.2 注册示例

// examples/tool_registration/main.go package main import ( "context" "encoding/json" "fmt" "time" "your-project/tools" ) func main() { registry := tools.NewRegistry() // 注册搜索工具 searchSpec := tools.ToolSpec{ Meta: tools.ToolMeta{ Name: "web_search", Version: "1.0.0", Description: "搜索互联网获取最新信息", Category: "external_api", Tags: []string{"search", "web"}, RiskLevel: "low", }, Parameters: json.RawMessage(`{ "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "num_results": {"type": "integer", "default": 5} }, "required": ["query"] }`), Returns: json.RawMessage(`{ "type": "array", "items": { "type": "object", "properties": { "title": {"type": "string"}, "url": {"type": "string"}, "snippet": {"type": "string"} } } }`), Timeout: 10 * time.Second, RateLimit: tools.RateLimitConfig{ MaxCallsPerMinute: 30, MaxCallsPerHour: 500, MaxCallsPerDay: 5000, }, } err := registry.Register(searchSpec, func(ctx context.Context, input json.RawMessage) (string, error) { var params struct { Query string `json:"query"` NumResults int `json:"num_results"` } if err := json.Unmarshal(input, &params); err != nil { return "", err } // 实际调用搜索 API return fmt.Sprintf(`[{"title":"%s 搜索结果","url":"https://example.com","snippet":"..."}]`, params.Query), nil }) if err != nil { panic(err) } fmt.Println("Search tool registered successfully") // 列出所有工具 for _, spec := range registry.List() { fmt.Printf(" - %s (%s): %s\n", spec.Meta.Name, spec.Meta.Version, spec.Meta.Description) } }

三、参数校验器

3.1 JSON Schema 校验

// tools/validator.go package tools import ( "encoding/json" "fmt" "regexp" "strings" "github.com/xeipuuv/gojsonschema" ) // Validator 参数校验器 type Validator struct { strictMode bool rules map[string]ValidationRule } type ValidationRule struct { Field string Check func(value interface{}) error Severity string // error / warning } func NewValidator(strictMode bool) *Validator { return &Validator{ strictMode: strictMode, rules: make(map[string]ValidationRule), } } // AddRule 添加自定义校验规则 func (v *Validator) AddRule(rule ValidationRule) { v.rules[rule.Field] = rule } // Validate 校验参数 func (v *Validator) Validate(schema json.RawMessage, input json.RawMessage) error { // 1. JSON Schema 校验 schemaLoader := gojsonschema.NewBytesLoader(schema) inputLoader := gojsonschema.NewBytesLoader(input) result, err := gojsonschema.Validate(schemaLoader, inputLoader) if err != nil { return fmt.Errorf("schema validation error: %w", err) } if !result.Valid() { var errs []string for _, desc := range result.Errors() { errs = append(errs, desc.String()) } return fmt.Errorf("validation failed: %s", strings.Join(errs, "; ")) } // 2. 自定义规则校验 var inputMap map[string]interface{} if err := json.Unmarshal(input, &inputMap); err != nil { return err } for field, rule := range v.rules { if value, exists := inputMap[field]; exists { if err := rule.Check(value); err != nil { return fmt.Errorf("custom rule [%s]: %w", field, err) } } } return nil } // 预置的安全校验规则 // NoSQLInjection 检查 SQL 注入 func NoSQLInjection() ValidationRule { return ValidationRule{ Field: "query", Check: func(value interface{}) error { str, ok := value.(string) if !ok { return nil } dangerous := []string{"DROP", "DELETE", "TRUNCATE", "ALTER", "EXEC", "--", ";", "/*"} upper := strings.ToUpper(str) for _, d := range dangerous { if strings.Contains(upper, d) { return fmt.Errorf("potential SQL injection detected: contains '%s'", d) } } return nil }, Severity: "error", } } // NoPathTraversal 检查路径穿越 func NoPathTraversal() ValidationRule { return ValidationRule{ Field: "path", Check: func(value interface{}) error { str, ok := value.(string) if !ok { return nil } if strings.Contains(str, "..") { return fmt.Errorf("path traversal detected") } if strings.HasPrefix(str, "/") || strings.HasPrefix(str, "~") { return fmt.Errorf("absolute path not allowed") } return nil }, Severity: "error", } } // MaxLength 检查字符串长度 func MaxLength(maxLen int) ValidationRule { return ValidationRule{ Field: "", Check: func(value interface{}) error { str, ok := value.(string) if !ok { return nil } if len([]rune(str)) > maxLen { return fmt.Errorf("length exceeds maximum %d characters", maxLen) } return nil }, Severity: "warning", } } // AllowedValues 检查枚举值 func AllowedValues(allowed []string) ValidationRule { return ValidationRule{ Field: "", Check: func(value interface{}) error { str, ok := value.(string) if !ok { return nil } for _, a := range allowed { if str == a { return nil } } return fmt.Errorf("value '%s' not in allowed list: %v", str, allowed) }, Severity: "error", } } // PatternMatch 正则匹配 func PatternMatch(pattern string) ValidationRule { re := regexp.MustCompile(pattern) return ValidationRule{ Field: "", Check: func(value interface{}) error { str, ok := value.(string) if !ok { return nil } if !re.MatchString(str) { return fmt.Errorf("value does not match pattern: %s", pattern) } return nil }, Severity: "error", } }

四、安全沙箱

4.1 代码执行沙箱

// tools/sandbox.go package tools import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" ) // SandboxConfig 沙箱配置 type SandboxConfig struct { WorkDir string // 工作目录 MaxMemory int64 // 最大内存(bytes) MaxCPUTime time.Duration // CPU 时间限制 MaxProcesses int // 最大进程数 NetworkAccess bool // 是否允许网络 ReadOnlyPaths []string // 只读路径 WriteBlacklist []string // 禁止写入路径 AllowedCommands []string // 允许的命令列表 } // Sandbox 沙箱执行器 type Sandbox struct { config SandboxConfig } func NewSandbox(config SandboxConfig) *Sandbox { return &Sandbox{ config: config, } } // Execute 在沙箱中执行命令 func (s *Sandbox) Execute(ctx context.Context, command string, args []string) (string, string, error) { // 1. 命令白名单检查 if !s.isCommandAllowed(command) { return "", "", fmt.Errorf("command '%s' is not in whitelist", command) } // 2. 创建工作目录 workDir, err := os.MkdirTemp(s.config.WorkDir, "sandbox-*") if err != nil { return "", "", fmt.Errorf("create sandbox dir: %w", err) } defer os.RemoveAll(workDir) // 3. 创建命令 cmd := exec.CommandContext(ctx, command, args...) cmd.Dir = workDir // 4. 资源限制 cmd.SysProcAttr = &syscall.SysProcAttr{ Setpgid: true, } // 5. 捕获输出 var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // 6. 执行 start := time.Now() err = cmd.Run() duration := time.Since(start) // 7. 超时检查 if err != nil { if ctx.Err() == context.DeadlineExceeded { // 杀死进程组 syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) return stdout.String(), stderr.String(), fmt.Errorf("execution timed out after %v", duration) } } return stdout.String(), stderr.String(), err } // ExecutePython 执行 Python 代码 func (s *Sandbox) ExecutePython(ctx context.Context, code string) (string, string, error) { // 写入临时文件 tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("sandbox_%d.py", time.Now().UnixNano())) if err := os.WriteFile(tmpFile, []byte(code), 0644); err != nil { return "", "", err } defer os.Remove(tmpFile) return s.Execute(ctx, "python3", []string{ "-c", "import sys; sys.path.insert(0, '/usr/lib/python3'); exec(open('" + tmpFile + "').read())", }) } // ExecuteJavaScript 执行 JavaScript 代码 func (s *Sandbox) ExecuteJavaScript(ctx context.Context, code string) (string, string, error) { return s.Execute(ctx, "node", []string{"-e", code}) } func (s *Sandbox) isCommandAllowed(cmd string) bool { if len(s.config.AllowedCommands) == 0 { return true // 默认允许所有 } base := filepath.Base(cmd) for _, allowed := range s.config.AllowedCommands { if base == allowed { return true } } return false } // 资源限制辅助函数(Linux cgroups) func (s *Sandbox) applyResourceLimits(pid int) error { // 内存限制 if s.config.MaxMemory > 0 { memLimit := strconv.FormatInt(s.config.MaxMemory, 10) if err := os.WriteFile( fmt.Sprintf("/sys/fs/cgroup/memory/agent-%d/memory.limit_in_bytes", pid), []byte(memLimit), 0644, ); err != nil { return fmt.Errorf("set memory limit: %w", err) } } // CPU 时间限制 if s.config.MaxCPUTime > 0 { cpuLimit := strconv.FormatInt(int64(s.config.MaxCPUTime.Seconds()), 10) if err := os.WriteFile( fmt.Sprintf("/sys/fs/cgroup/cpu/agent-%d/cpu.cfs_quota_us", pid), []byte(cpuLimit), 0644, ); err != nil { return fmt.Errorf("set CPU limit: %w", err) } } return nil }

4.2 文件系统隔离

// tools/fs_sandbox.go package tools import ( "fmt" "os" "path/filepath" "strings" ) // FileSystemGuard 文件系统守卫 type FileSystemGuard struct { allowedDirs []string forbiddenExts []string maxFileSize int64 } func NewFileSystemGuard(allowedDirs []string) *FileSystemGuard { return &FileSystemGuard{ allowedDirs: allowedDirs, forbiddenExts: []string{".exe", ".dll", ".so", ".dylib", ".bin"}, maxFileSize: 10 * 1024 * 1024, // 10MB } } // ValidatePath 验证路径是否合法 func (g *FileSystemGuard) ValidatePath(path string) error { // 1. 规范化路径 cleanPath := filepath.Clean(path) // 2. 检查路径穿越 if strings.Contains(cleanPath, "..") { return fmt.Errorf("path traversal detected: %s", path) } // 3. 检查是否在允许目录内 allowed := false for _, dir := range g.allowedDirs { absDir, _ := filepath.Abs(dir) absPath, _ := filepath.Abs(cleanPath) if strings.HasPrefix(absPath, absDir) { allowed = true break } } if !allowed { return fmt.Errorf("path outside allowed directories: %s", path) } // 4. 检查文件扩展名 ext := filepath.Ext(cleanPath) for _, forbidden := range g.forbiddenExts { if ext == forbidden { return fmt.Errorf("forbidden file extension: %s", ext) } } return nil } // SafeReadFile 安全读取文件 func (g *FileSystemGuard) SafeReadFile(path string) ([]byte, error) { if err := g.ValidatePath(path); err != nil { return nil, err } info, err := os.Stat(path) if err != nil { return nil, err } // 检查文件大小 if info.Size() > g.maxFileSize { return nil, fmt.Errorf("file too large: %d bytes (max %d)", info.Size(), g.maxFileSize) } return os.ReadFile(path) } // SafeWriteFile 安全写入文件 func (g *FileSystemGuard) SafeWriteFile(path string, data []byte) error { if err := g.ValidatePath(path); err != nil { return err } if int64(len(data)) > g.maxFileSize { return fmt.Errorf("data too large: %d bytes (max %d)", len(data), g.maxFileSize) } return os.WriteFile(path, data, 0644) }

五、限流器

// tools/limiter.go package tools import ( "fmt" "sync" "time" ) // RateLimiter 限流器 type RateLimiter struct { mu sync.Mutex windows map[string]*SlidingWindow } type SlidingWindow struct { timestamps []time.Time maxCount int windowSize time.Duration } func NewRateLimiter() *RateLimiter { return &RateLimiter{ windows: make(map[string]*SlidingWindow), } } // Allow 检查是否允许调用 func (rl *RateLimiter) Allow(key string, maxCount int, window time.Duration) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() sw, exists := rl.windows[key] if !exists { sw = &SlidingWindow{ timestamps: make([]time.Time, 0), maxCount: maxCount, windowSize: window, } rl.windows[key] = sw } // 清理过期的时间戳 cutoff := now.Add(-window) valid := make([]time.Time, 0) for _, ts := range sw.timestamps { if ts.After(cutoff) { valid = append(valid, ts) } } sw.timestamps = valid // 检查是否超过限制 if len(sw.timestamps) >= maxCount { return false } // 记录本次调用 sw.timestamps = append(sw.timestamps, now) return true } // ToolRateLimiter 工具级别限流器 type ToolRateLimiter struct { limiter *RateLimiter toolLimits map[string]RateLimitConfig } func NewToolRateLimiter() *ToolRateLimiter { return &ToolRateLimiter{ limiter: NewRateLimiter(), toolLimits: make(map[string]RateLimitConfig), } } // SetLimit 设置工具限流配置 func (trl *ToolRateLimiter) SetLimit(toolName string, config RateLimitConfig) { trl.toolLimits[toolName] = config } // CheckLimit 检查工具是否达到限流 func (trl *ToolRateLimiter) CheckLimit(toolName string) error { config, exists := trl.toolLimits[toolName] if !exists { return nil // 没有限流配置 } minuteKey := fmt.Sprintf("%s:minute", toolName) hourKey := fmt.Sprintf("%s:hour", toolName) dayKey := fmt.Sprintf("%s:day", toolName) if !trl.limiter.Allow(minuteKey, config.MaxCallsPerMinute, time.Minute) { return fmt.Errorf("rate limit exceeded for %s: max %d calls/minute", toolName, config.MaxCallsPerMinute) } if !trl.limiter.Allow(hourKey, config.MaxCallsPerHour, time.Hour) { return fmt.Errorf("rate limit exceeded for %s: max %d calls/hour", toolName, config.MaxCallsPerHour) } if !trl.limiter.Allow(dayKey, config.MaxCallsPerDay, 24*time.Hour) { return fmt.Errorf("rate limit exceeded for %s: max %d calls/day", toolName, config.MaxCallsPerDay) } return nil } // GlobalRateLimiter 全局限流器 type GlobalRateLimiter struct { limiter *RateLimiter maxConcurrent int activeCount int32 } func NewGlobalRateLimiter(maxConcurrent int) *GlobalRateLimiter { return &GlobalRateLimiter{ limiter: NewRateLimiter(), maxConcurrent: maxConcurrent, } } // Acquire 获取并发许可 func (grl *GlobalRateLimiter) Acquire() bool { if atomic.LoadInt32(&grl.activeCount) >= int32(grl.maxConcurrent) { return false } atomic.AddInt32(&grl.activeCount, 1) return true } // Release 释放并发许可 func (grl *GlobalRateLimiter) Release() { atomic.AddInt32(&grl.activeCount, -1) }

六、完整的工具执行流程

// tools/executor.go package tools import ( "context" "encoding/json" "fmt" "time" ) // ToolExecutor 工具执行器 type ToolExecutor struct { registry *Registry validator *Validator limiter *ToolRateLimiter tracer Tracer } type Tracer interface { StartSpan(ctx context.Context, name string) (context.Context, Span) } type Span interface { End() SetAttribute(key string, value interface{}) RecordError(err error) } func NewToolExecutor(registry *Registry, validator *Validator, limiter *ToolRateLimiter, tracer Tracer) *ToolExecutor { return &ToolExecutor{ registry: registry, validator: validator, limiter: limiter, tracer: tracer, } } // Execute 执行工具调用(完整链路) func (te *ToolExecutor) Execute(ctx context.Context, toolName string, input json.RawMessage) (string, error) { ctx, span := te.tracer.StartSpan(ctx, fmt.Sprintf("tool.%s", toolName)) defer span.End() span.SetAttribute("tool.name", toolName) span.SetAttribute("tool.input", string(input)) // 1. 查找工具 tool, err := te.registry.Get(toolName) if err != nil { span.RecordError(err) return "", fmt.Errorf("tool lookup: %w", err) } // 2. 限流检查 if err := te.limiter.CheckLimit(toolName); err != nil { span.RecordError(err) return "", fmt.Errorf("rate limit: %w", err) } // 3. 参数校验 if err := te.validator.Validate(tool.Spec.Parameters, input); err != nil { span.RecordError(err) return "", fmt.Errorf("parameter validation: %w", err) } // 4. 设置超时上下文 execCtx, cancel := context.WithTimeout(ctx, tool.Spec.Timeout) defer cancel() // 5. 执行 start := time.Now() output, err := tool.Execute(execCtx, input) duration := time.Since(start) span.SetAttribute("tool.duration_ms", duration.Milliseconds()) span.SetAttribute("tool.output_length", len(output)) if err != nil { span.RecordError(err) return "", fmt.Errorf("execution: %w", err) } return output, nil }

七、本讲总结

┌─────────────────────────────────────────────────────────────┐ │ 第3讲 核心收获 │ │ │ │ ✅ 工具注册中心:名称路由、版本管理、分类标签、负责人 │ │ ✅ 参数校验器:JSON Schema 校验 + 自定义规则 │ │ - SQL 注入检测、路径穿越防护、长度限制、枚举值检查 │ │ ✅ 安全沙箱: │ │ - 命令白名单、临时工作目录、资源限制(CPU/内存) │ │ - 文件系统隔离(路径验证、扩展名过滤、大小限制) │ │ ✅ 限流器: │ │ - 滑动窗口算法(分钟/小时/天三级限流) │ │ - 全局并发控制 │ │ ✅ 完整执行链路:查找 → 限流 → 校验 → 超时 → 执行 → 追踪 │ │ │ └─────────────────────────────────────────────────────────────┘

八、下一讲预告

第4讲:《Agent 记忆系统:短期上下文、长期记忆与遗忘策略》

你将学到:

  • 短期记忆:上下文窗口管理与压缩
  • 长期记忆:向量库存储关键信息
  • 记忆检索:什么时候该记,什么时候该忘
  • 会话记忆 vs 跨会话记忆
  • 完整的记忆系统实现

🧰 开发之余的小工具推荐

调试 Agent 时经常需要查看 JSON 格式的工具参数和返回结果。我用 zz365.top 的 JSON 格式化器和在线工具来处理这些,所有数据都在浏览器本地处理,不上传服务器。免费无广告,适合开发时随手使用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询