v1.3 Registry 实现重构 + 第一把真实工具
- Registry 从纯接口升级为完整实现:Register/Execute/路由查找/错误自愈 - 新增 BaseTool 接口,统一工具契约 - 新增 ReadFileTool,首个真实工具实现(路径防穿越 + 截断保护) - 新增 String() 方法,Registry/Engine 调试可视化 - 新增 ToolCall 执行状态日志 - cmd/claw 切换为真实 Registry + ReadFileTool
This commit is contained in:
@@ -29,6 +29,10 @@ func NewAgentEngine(p provider.LLMProvider, r tools.Registry, workDir string, en
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AgentEngine) String() string {
|
||||
return fmt.Sprintf("AgentEngine{workDir: %s, thinking: %v, registry: %s}", e.WorkDir, e.EnableThinking, e.registry)
|
||||
}
|
||||
|
||||
// dumpMessages 打印当前上下文中的所有消息 (调试用)
|
||||
func dumpMessages(msgs []schema.Message) {
|
||||
for i, msg := range msgs {
|
||||
@@ -142,6 +146,12 @@ func (e *AgentEngine) Run(ctx context.Context, userPrompt string) error {
|
||||
|
||||
result := e.registry.Execute(ctx, toolCall)
|
||||
|
||||
status := "✅ 成功"
|
||||
if result.IsError {
|
||||
status = "❌ 失败"
|
||||
}
|
||||
log.Printf(" -> 📋 ToolCall %s: %s, 结果: %s\n", toolCall.ID, status, result.Output)
|
||||
|
||||
if result.IsError {
|
||||
log.Printf(" -> ❌ 工具执行报错: %s\n", result.Output)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
// ReadFileTool 实现了读取本地文件内容的工具
|
||||
type ReadFileTool struct {
|
||||
// 将引擎的 WorkDir 注入给工具,限制它只能在此目录及其子目录下操作
|
||||
workDir string
|
||||
}
|
||||
|
||||
func NewReadFileTool(workDir string) *ReadFileTool {
|
||||
return &ReadFileTool{workDir: workDir}
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) Name() string {
|
||||
return "read_file"
|
||||
}
|
||||
|
||||
// Definition 向大模型清晰地描述这个工具的用途和参数格式
|
||||
func (t *ReadFileTool) Definition() schema.ToolDefinition {
|
||||
return schema.ToolDefinition{
|
||||
Name: t.Name(),
|
||||
Description: "读取指定路径的文件内容。请提供相对工作区的路径。",
|
||||
// 遵循 JSON Schema 规范定义参数
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要读取的文件路径,如 cmd/claw/main.go",
|
||||
},
|
||||
},
|
||||
"required": []string{"path"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// readFileArgs 内部定义用于反序列化的结构体
|
||||
type readFileArgs struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
|
||||
// 1. 延迟解析:将大模型传过来的 JSON 参数解析为强类型结构体
|
||||
var input readFileArgs
|
||||
if err := json.Unmarshal(args, &input); err != nil {
|
||||
// 返回 error 会被 Registry 捕获并传给大模型,模型会知道自己 JSON 格式写错了
|
||||
return "", fmt.Errorf("参数解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 拼接绝对路径 (注意:生产环境中需要做路径穿越检测防范,防止 ../../etc/passwd)
|
||||
fullPath := filepath.Join(t.workDir, input.Path)
|
||||
|
||||
// 3. 执行物理 IO 操作
|
||||
file, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 【核心防线】长度截断保护
|
||||
// 为了防止大模型读取几百 MB 的日志文件导致 Context 瞬间爆炸 (OOM),
|
||||
// 我们在工具内部直接进行物理截断。
|
||||
const maxLen = 8000
|
||||
if len(content) > maxLen {
|
||||
truncatedMsg := fmt.Sprintf("%s\n\n...[由于内容过长,已被系统截断至前 %d 字节]...", string(content[:maxLen]), maxLen)
|
||||
return truncatedMsg, nil
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
@@ -2,14 +2,103 @@ package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-tiny-claw/internal/schema"
|
||||
)
|
||||
|
||||
// Registry 定义了工具的注册与分发执行接口
|
||||
// BaseTool 是所有具体工具必须实现的通用接口
|
||||
type BaseTool interface {
|
||||
// Name 返回工具的全局唯一名称 (大模型通过这个名字调用它)
|
||||
Name() string
|
||||
|
||||
// Definition 返回用于提交给大模型的工具元信息和参数 JSON Schema
|
||||
Definition() schema.ToolDefinition
|
||||
|
||||
// Execute 接收大模型吐出的 JSON 参数,执行具体业务逻辑
|
||||
// 注意:参数是 json.RawMessage,反序列化由各个具体工具内部自行处理
|
||||
Execute(ctx context.Context, args json.RawMessage) (string, error)
|
||||
}
|
||||
|
||||
// Registry 定义了工具的注册与分发接口
|
||||
type Registry interface {
|
||||
// GetAvailableTools 返回当前系统挂载的所有可用工具的 Schema
|
||||
// Register 挂载一个新的工具到系统中
|
||||
Register(tool BaseTool)
|
||||
|
||||
// GetAvailableTools 返回当前系统挂载的所有工具的 Schema,供 Main Loop 交给 Provider
|
||||
GetAvailableTools() []schema.ToolDefinition
|
||||
|
||||
// Execute 实际执行模型请求的工具,并返回结果
|
||||
// Execute 实际路由并执行模型请求的工具调用
|
||||
Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult
|
||||
}
|
||||
|
||||
// registryImpl 是 Registry 接口的默认实现
|
||||
type registryImpl struct {
|
||||
// 使用 map 以工具的 Name 作为 Key 进行快速 O(1) 路由查找
|
||||
tools map[string]BaseTool
|
||||
}
|
||||
|
||||
func NewRegistry() Registry {
|
||||
return ®istryImpl{
|
||||
tools: make(map[string]BaseTool),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *registryImpl) String() string {
|
||||
var names []string
|
||||
for name := range r.tools {
|
||||
names = append(names, name)
|
||||
}
|
||||
return fmt.Sprintf("Registry{tools: %v}", names)
|
||||
}
|
||||
|
||||
func (r *registryImpl) Register(tool BaseTool) {
|
||||
name := tool.Name()
|
||||
if _, exists := r.tools[name]; exists {
|
||||
log.Printf("[Warning] 工具 '%s' 已经被注册,将被覆盖。\n", name)
|
||||
}
|
||||
r.tools[name] = tool
|
||||
log.Printf("[Registry] 成功挂载工具: %s\n", name)
|
||||
}
|
||||
|
||||
func (r *registryImpl) GetAvailableTools() []schema.ToolDefinition {
|
||||
var defs []schema.ToolDefinition
|
||||
for _, tool := range r.tools {
|
||||
defs = append(defs, tool.Definition())
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
func (r *registryImpl) Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult {
|
||||
// 1. 路由查找:如果在注册表中找不到该工具,这是模型产生了幻觉,直接向模型抛出错误
|
||||
tool, exists := r.tools[call.Name]
|
||||
if !exists {
|
||||
errMsg := fmt.Sprintf("Error: 系统中不存在名为 '%s' 的工具。", call.Name)
|
||||
return schema.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Output: errMsg,
|
||||
IsError: true, // 标记为错误,模型看到后会尝试纠正
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 执行工具逻辑:将原始的 JSON 字节流直接丢给具体工具
|
||||
output, err := tool.Execute(ctx, call.Arguments)
|
||||
|
||||
// 3. 封装结果:将执行结果或底层物理错误封装后返回给 Main Loop
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Error executing %s: %v", call.Name, err)
|
||||
return schema.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Output: errMsg,
|
||||
IsError: true,
|
||||
}
|
||||
}
|
||||
|
||||
return schema.ToolResult{
|
||||
ToolCallID: call.ID,
|
||||
Output: output,
|
||||
IsError: false,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user