支持工具调用:三个AI角色配置、工具注册表与流式工具轮

This commit is contained in:
2026-08-14 16:55:48 +08:00
parent ce49bb9c1e
commit 1c9e98ddb0
11 files changed
+491 -28

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
package tools
import (
"encoding/json"
"fmt"
"math"
"strconv"
"github.com/expr-lang/expr"
)
type CalculatorTool struct{}
func (CalculatorTool) Name() string { return "calculate" }
func (CalculatorTool) Description() string {
return "计算数学表达式,如 \"(12 + 3) * 4\"、\"2^10\"、\"sqrt(9)\""
}
func (CalculatorTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"expression": map[string]any{"type": "string", "description": "要计算的数学表达式"},
},
"required": []string{"expression"},
}
}
func (CalculatorTool) Execute(args json.RawMessage) (string, error) {
var p struct {
Expression string `json:"expression"`
}
if err := json.Unmarshal(args, &p); err != nil {
return "", err
}
if p.Expression == "" {
return "", fmt.Errorf("expression 不能为空")
}
out, err := expr.Eval(p.Expression, nil)
if err != nil {
return "", err
}
switch v := out.(type) {
case float64:
return strconv.FormatFloat(round(v), 'f', -1, 64), nil
case int:
return strconv.Itoa(v), nil
case bool:
return strconv.FormatBool(v), nil
case string:
return v, nil
default:
return fmt.Sprintf("%v", v), nil
}
}
func round(v float64) float64 {
if math.IsInf(v, 0) || math.IsNaN(v) {
return v
}
return math.Round(v*1e8) / 1e8
}
+42
View File
@@ -0,0 +1,42 @@
package tools
import (
"encoding/json"
"fmt"
"math/rand/v2"
)
type RandomTool struct{}
func (RandomTool) Name() string { return "random_number" }
func (RandomTool) Description() string {
return "生成指定范围内的随机整数,默认 0 到 100(含端点)"
}
func (RandomTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"min": map[string]any{"type": "integer", "description": "最小值,默认 0"},
"max": map[string]any{"type": "integer", "description": "最大值,默认 100"},
},
}
}
func (RandomTool) Execute(args json.RawMessage) (string, error) {
var p struct {
Min *int `json:"min"`
Max *int `json:"max"`
}
_ = json.Unmarshal(args, &p)
min, max := 0, 100
if p.Min != nil {
min = *p.Min
}
if p.Max != nil {
max = *p.Max
}
if max < min {
return "", fmt.Errorf("max (%d) 不能小于 min (%d)", max, min)
}
return fmt.Sprintf("%d", rand.IntN(max-min+1)+min), nil
}
+38
View File
@@ -0,0 +1,38 @@
package tools
import (
"encoding/json"
"time"
)
type TimeTool struct{}
func (TimeTool) Name() string { return "get_current_time" }
func (TimeTool) Description() string {
return "获取当前日期和时间,可选指定时区(如 Asia/Shanghai,默认本地时区)"
}
func (TimeTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"timezone": map[string]any{"type": "string", "description": "IANA 时区名,如 Asia/Shanghai"},
},
}
}
func (TimeTool) Execute(args json.RawMessage) (string, error) {
var p struct {
Timezone string `json:"timezone"`
}
_ = json.Unmarshal(args, &p)
loc := time.Local
if p.Timezone != "" {
l, err := time.LoadLocation(p.Timezone)
if err != nil {
return "", err
}
loc = l
}
now := time.Now().In(loc)
return now.Format("2006-01-02 15:04:05 Monday MST"), nil
}
+56
View File
@@ -0,0 +1,56 @@
package tools
import (
"encoding/json"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/packages/param"
"github.com/openai/openai-go/shared"
)
type Tool interface {
Name() string
Description() string
Parameters() map[string]any
Execute(args json.RawMessage) (string, error)
}
type Registry struct {
tools map[string]Tool
}
func NewRegistry(t ...Tool) *Registry {
r := &Registry{tools: make(map[string]Tool)}
for _, tool := range t {
r.tools[tool.Name()] = tool
}
return r
}
func (r *Registry) Get(name string) (Tool, bool) {
t, ok := r.tools[name]
return t, ok
}
func (r *Registry) ParamList() []openai.ChatCompletionToolParam {
out := make([]openai.ChatCompletionToolParam, 0, len(r.tools))
for _, t := range r.tools {
out = append(out, openai.ChatCompletionToolParam{
Function: shared.FunctionDefinitionParam{
Name: t.Name(),
Description: param.NewOpt(t.Description()),
Parameters: shared.FunctionParameters(t.Parameters()),
},
})
}
return out
}
func (r *Registry) Execute(name string, args json.RawMessage) (string, error) {
t, ok := r.tools[name]
if !ok {
return "", fmt.Errorf("未知工具: %s", name)
}
return t.Execute(args)
}
+87
View File
@@ -0,0 +1,87 @@
package tools
import (
"encoding/json"
"strings"
"testing"
)
func TestCalculator(t *testing.T) {
cases := []struct {
name string
expr string
want string
}{
{"四则运算", "(12 + 3) * 4", "60"},
{"幂运算", "2^10", "1024"},
{"浮点", "7 / 2", "3.5"},
{"小数精度", "1 / 3", "0.33333333"},
{"布尔", "2 > 1", "true"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
args, _ := json.Marshal(map[string]string{"expression": c.expr})
got, err := CalculatorTool{}.Execute(args)
if err != nil {
t.Fatalf("Execute 出错: %v", err)
}
if got != c.want {
t.Errorf("Execute(%q) = %q, want %q", c.expr, got, c.want)
}
})
}
}
func TestCalculatorInvalid(t *testing.T) {
args, _ := json.Marshal(map[string]string{"expression": "1 +"})
if _, err := (CalculatorTool{}).Execute(args); err == nil {
t.Error("非法表达式应返回错误")
}
}
func TestRandom(t *testing.T) {
args, _ := json.Marshal(map[string]any{"min": 1, "max": 10})
for i := 0; i < 100; i++ {
out, err := RandomTool{}.Execute(args)
if err != nil {
t.Fatalf("Execute 出错: %v", err)
}
var v int
if err := json.Unmarshal([]byte(out), &v); err != nil {
t.Fatalf("结果 %q 解析失败: %v", out, err)
}
if v < 1 || v > 10 {
t.Fatalf("结果 %q 不在 [1,10] 内", out)
}
}
bad, _ := json.Marshal(map[string]any{"min": 10, "max": 1})
if _, err := (RandomTool{}).Execute(bad); err == nil {
t.Error("min>max 应返回错误")
}
}
func TestTimeTool(t *testing.T) {
out, err := TimeTool{}.Execute(nil)
if err != nil {
t.Fatalf("Execute 出错: %v", err)
}
if !strings.Contains(out, "20") {
t.Errorf("时间输出异常: %q", out)
}
}
func TestRegistry(t *testing.T) {
r := NewRegistry(TimeTool{}, CalculatorTool{}, RandomTool{})
if _, ok := r.Get("get_current_time"); !ok {
t.Error("get_current_time 未注册")
}
if _, ok := r.Get("nonexistent"); ok {
t.Error("未知工具不应存在")
}
if len(r.ParamList()) != 3 {
t.Errorf("ParamList 数量 = %d, want 3", len(r.ParamList()))
}
if _, err := r.Execute("nonexistent", nil); err == nil {
t.Error("执行未知工具应返回错误")
}
}