Files
nofx/mcp/intro/BUILDER_PATTERN_BENEFITS.md
Shui 88b01c8f2a refactor(mcp) (#1042)
* improve(interface): replace with interface

* feat(mcp): 添加构建器模式支持

新增功能:
- RequestBuilder 构建器,支持流式 API
- 多轮对话支持(AddAssistantMessage)
- Function Calling / Tools 支持
- 精细参数控制(temperature, top_p, penalties 等)
- 3个预设场景(Chat, CodeGen, CreativeWriting)
- 完整的测试套件(19个新测试)

修复问题:
- Config 字段未使用(MaxRetries、Temperature 等)
- DeepSeek/Qwen SetAPIKey 的冗余 nil 检查

向后兼容:
- 保留 CallWithMessages API
- 新增 CallWithRequest API

测试:
- 81 个测试全部通过
- 覆盖率 80.6%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

---------

Co-authored-by: zbhan <zbhan@freewheel.tv>
Co-authored-by: tinkle-community <tinklefund@gmail.com>
2025-11-15 23:04:53 -05:00

17 KiB
Raw Permalink Blame History

构建器模式在 MCP 模块中的应用价值

📋 目录

  1. 当前实现的局限性
  2. 构建器模式的好处
  3. 实际应用场景
  4. 对比示例
  5. 是否需要引入

当前实现的局限性

现状分析

当前 buildMCPRequestBody 实现:

func (client *Client) buildMCPRequestBody(systemPrompt, userPrompt string) map[string]any {
    messages := []map[string]string{}

    if systemPrompt != "" {
        messages = append(messages, map[string]string{
            "role":    "system",
            "content": systemPrompt,
        })
    }
    messages = append(messages, map[string]string{
        "role":    "user",
        "content": userPrompt,
    })

    return map[string]interface{}{
        "model":       client.Model,
        "messages":    messages,
        "temperature": client.config.Temperature,
        "max_tokens":  client.MaxTokens,
    }
}

存在的限制

  1. 只支持简单对话

    • 无法添加多轮对话历史
    • 无法添加 assistant 回复
    • 无法构建复杂的对话上下文
  2. 参数固定

    • 无法动态添加可选参数(如 top_p、frequency_penalty
    • 无法为单次请求自定义 temperature会影响全局配置
    • 无法添加 function calling、tools 等高级功能
  3. 扩展性差

    • 每次添加新参数都需要修改方法签名
    • 参数列表会越来越长
    • 子类重写时需要处理所有参数

构建器模式的好处

1. 🎯 灵活性和可读性

当前方式(参数传递)

// 问题:参数多了会很混乱
client.CallWithCustomParams(
    "system prompt",
    "user prompt",
    0.8,              // temperature - 这是什么?
    2000,             // max_tokens - 这是什么?
    0.9,              // top_p - 这是什么?
    0.5,              // frequency_penalty
    nil,              // stop sequences
    false,            // stream
)

构建器方式

// 清晰、自解释
request := NewRequestBuilder().
    WithSystemPrompt("You are a helpful assistant").
    WithUserPrompt("Tell me about Go").
    WithTemperature(0.8).
    WithMaxTokens(2000).
    WithTopP(0.9).
    Build()

result, err := client.CallWithRequest(request)

2. 📚 支持复杂场景

场景1: 多轮对话

当前方式: 😢 不支持

// ❌ 无法实现
client.CallWithMessages("system", "user prompt")

构建器方式: 支持

request := NewRequestBuilder().
    AddSystemMessage("You are a helpful assistant").
    AddUserMessage("What is the weather?").
    AddAssistantMessage("It's sunny today").
    AddUserMessage("What about tomorrow?").  // 继续对话
    WithTemperature(0.7).
    Build()

场景2: 函数调用Function Calling

当前方式: 😢 不支持

// ❌ 无法添加 tools/functions

构建器方式: 支持

request := NewRequestBuilder().
    WithUserPrompt("What's the weather in Beijing?").
    AddTool(Tool{
        Type: "function",
        Function: FunctionDef{
            Name:        "get_weather",
            Description: "Get current weather",
            Parameters:  weatherParamsSchema,
        },
    }).
    WithToolChoice("auto").
    Build()

场景3: 流式响应

当前方式: 😢 需要修改整个架构

// ❌ CallWithMessages 不支持流式

构建器方式: 易于扩展

request := NewRequestBuilder().
    WithUserPrompt("Write a long story").
    WithStream(true).
    Build()

stream, err := client.CallStream(request)
for chunk := range stream {
    fmt.Print(chunk)
}

3. 🔧 易于扩展和维护

添加新参数

当前方式: 😢 破坏性修改

// 需要修改方法签名(破坏现有代码)
func (client *Client) buildMCPRequestBody(
    systemPrompt, userPrompt string,
    // 新增参数会导致所有调用处都要修改
    topP float64,
    presencePenalty float64,
) map[string]any

构建器方式: 向后兼容

// 只需添加新方法,不影响现有代码
func (b *RequestBuilder) WithPresencePenalty(p float64) *RequestBuilder {
    b.presencePenalty = p
    return b
}

// 旧代码不受影响
request := builder.WithUserPrompt("Hello").Build()

// 新代码可以使用新功能
request := builder.
    WithUserPrompt("Hello").
    WithPresencePenalty(0.6).  // 新参数
    Build()

4. 🎨 可选参数处理

当前方式: 😢 难以处理可选参数

// 方案1: 传 nil/0 值(不优雅)
client.CallWithParams(system, user, 0, 0, nil, nil)

// 方案2: 使用选项模式(但每次调用都要传)
client.CallWithParams(system, user, WithTopP(0.9), WithPenalty(0.5))

// 方案3: 配置对象(需要创建临时对象)
config := &RequestConfig{
    SystemPrompt: system,
    UserPrompt:   user,
    TopP:         0.9,
}

构建器方式: 优雅处理

// 只设置需要的参数,其他使用默认值
request := NewRequestBuilder().
    WithUserPrompt("Hello").
    // 不设置 temperature使用默认值
    // 不设置 topP使用默认值
    Build()

// 也可以全部自定义
request := NewRequestBuilder().
    WithUserPrompt("Hello").
    WithTemperature(0.8).
    WithTopP(0.9).
    WithMaxTokens(2000).
    Build()

5. 类型安全和验证

当前方式: 😢 运行时才发现错误

// ❌ 编译时无法发现问题
client.CallWithMessages("", "")  // 空 prompt
client.CallWithMessages("system", "user")  // temperature 可能不合法

构建器方式: 提前验证

type RequestBuilder struct {
    messages    []Message
    temperature float64
    maxTokens   int
}

func (b *RequestBuilder) WithTemperature(t float64) *RequestBuilder {
    if t < 0 || t > 2 {
        panic("temperature must be between 0 and 2")  // 或返回 error
    }
    b.temperature = t
    return b
}

func (b *RequestBuilder) Build() (*Request, error) {
    if len(b.messages) == 0 {
        return nil, errors.New("at least one message is required")
    }
    if b.maxTokens <= 0 {
        return nil, errors.New("maxTokens must be positive")
    }
    return &Request{...}, nil
}

实际应用场景

场景1: 量化交易 AI 顾问(多轮对话)

// 构建包含市场数据的上下文对话
request := NewRequestBuilder().
    AddSystemMessage("You are a quantitative trading advisor").
    AddUserMessage("Analyze BTC trend").
    AddAssistantMessage("BTC is in an upward trend based on...").
    AddUserMessage("What about entry points?").  // 继续对话
    WithTemperature(0.3).  // 低温度,更精确
    WithMaxTokens(1000).
    Build()

analysis, err := client.CallWithRequest(request)

场景2: 代码生成(需要精确控制)

request := NewRequestBuilder().
    WithSystemPrompt("You are a Go expert").
    WithUserPrompt("Generate a HTTP server").
    WithTemperature(0.2).        // 低温度,更确定性
    WithTopP(0.1).               // 低 top_p更聚焦
    WithMaxTokens(2000).
    WithStopSequences([]string{"```"}).  // 遇到代码块结束符停止
    Build()

场景3: 创意写作(需要随机性)

request := NewRequestBuilder().
    WithSystemPrompt("You are a creative writer").
    WithUserPrompt("Write a sci-fi story").
    WithTemperature(1.2).        // 高温度,更创意
    WithTopP(0.95).              // 高 top_p更多样性
    WithPresencePenalty(0.6).    // 避免重复
    WithFrequencyPenalty(0.5).
    WithMaxTokens(4000).
    Build()

场景4: 函数调用(工具使用)

// 定义工具
weatherTool := Tool{
    Type: "function",
    Function: FunctionDef{
        Name:        "get_weather",
        Description: "Get current weather for a location",
        Parameters: map[string]any{
            "type": "object",
            "properties": map[string]any{
                "location": map[string]any{
                    "type":        "string",
                    "description": "City name",
                },
            },
            "required": []string{"location"},
        },
    },
}

request := NewRequestBuilder().
    WithUserPrompt("What's the weather in Beijing?").
    AddTool(weatherTool).
    WithToolChoice("auto").
    Build()

response, err := client.CallWithRequest(request)
// 解析 response.ToolCalls 并执行实际的天气查询

对比示例

示例1: 基础用法

当前实现

result, err := client.CallWithMessages(
    "You are a helpful assistant",
    "What is Go?",
)

构建器模式

request := NewRequestBuilder().
    WithSystemPrompt("You are a helpful assistant").
    WithUserPrompt("What is Go?").
    Build()

result, err := client.CallWithRequest(request)

分析: 基础用法下,构建器稍显冗长,但更清晰。


示例2: 复杂用法

当前实现(假设扩展后)

// 😢 参数太多,难以理解
result, err := client.CallWithMessagesAdvanced(
    "system prompt",
    "user prompt",
    nil,    // messages history?
    0.8,    // temperature
    2000,   // max_tokens
    0.9,    // top_p
    0.5,    // frequency_penalty
    0.6,    // presence_penalty
    nil,    // stop sequences
    false,  // stream
    nil,    // tools
    "",     // tool_choice
)

构建器模式

// ✅ 清晰、自解释
request := NewRequestBuilder().
    WithSystemPrompt("system prompt").
    WithUserPrompt("user prompt").
    WithTemperature(0.8).
    WithMaxTokens(2000).
    WithTopP(0.9).
    WithFrequencyPenalty(0.5).
    WithPresencePenalty(0.6).
    Build()

result, err := client.CallWithRequest(request)

分析: 复杂场景下,构建器模式优势明显。


是否需要引入?

建议引入的情况

  1. 需要支持多轮对话

    • 聊天机器人
    • 上下文相关的 AI 助手
  2. 需要精细控制 AI 参数

    • 不同任务需要不同 temperature
    • 需要使用 top_p、penalty 等高级参数
  3. 需要使用 AI 高级功能

    • Function Calling / Tools
    • 流式响应
    • Vision API图片输入
  4. API 接口可能频繁变化

    • AI 提供商经常添加新参数
    • 需要向后兼容

⚠️ 可以暂缓的情况

  1. 只有简单的单轮对话

    • 当前 CallWithMessages 已足够
  2. 参数固定不变

    • 所有请求使用相同配置
  3. 团队规模小,代码量少

    • 引入新模式的学习成本 > 收益

推荐方案

方案1: 渐进式引入(推荐)

第一阶段: 保留现有 API新增构建器

// 旧 API 继续工作(向后兼容)
result, err := client.CallWithMessages("system", "user")

// 新 API 提供高级功能
request := NewRequestBuilder().
    WithUserPrompt("user").
    WithTemperature(0.8).
    Build()
result, err := client.CallWithRequest(request)

第二阶段: 逐步迁移

// 在文档中推荐使用构建器
// 旧 API 标记为 Deprecated但不删除

方案2: 仅用于高级场景

只在需要复杂功能时使用构建器:

// 简单场景:使用现有 API
client.CallWithMessages("system", "user")

// 复杂场景:使用构建器
client.CallWithRequest(
    NewRequestBuilder().
        AddConversationHistory(history).
        AddUserMessage("new question").
        WithTools(tools).
        Build(),
)

实现示例

完整的构建器实现

package mcp

type Message struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type Tool struct {
    Type     string      `json:"type"`
    Function FunctionDef `json:"function"`
}

type Request struct {
    Model             string    `json:"model"`
    Messages          []Message `json:"messages"`
    Temperature       float64   `json:"temperature,omitempty"`
    MaxTokens         int       `json:"max_tokens,omitempty"`
    TopP              float64   `json:"top_p,omitempty"`
    FrequencyPenalty  float64   `json:"frequency_penalty,omitempty"`
    PresencePenalty   float64   `json:"presence_penalty,omitempty"`
    Stop              []string  `json:"stop,omitempty"`
    Tools             []Tool    `json:"tools,omitempty"`
    ToolChoice        string    `json:"tool_choice,omitempty"`
    Stream            bool      `json:"stream,omitempty"`
}

type RequestBuilder struct {
    model            string
    messages         []Message
    temperature      *float64
    maxTokens        *int
    topP             *float64
    frequencyPenalty *float64
    presencePenalty  *float64
    stop             []string
    tools            []Tool
    toolChoice       string
    stream           bool
}

func NewRequestBuilder() *RequestBuilder {
    return &RequestBuilder{
        messages: make([]Message, 0),
    }
}

func (b *RequestBuilder) WithModel(model string) *RequestBuilder {
    b.model = model
    return b
}

func (b *RequestBuilder) WithSystemPrompt(prompt string) *RequestBuilder {
    if prompt != "" {
        b.messages = append(b.messages, Message{
            Role:    "system",
            Content: prompt,
        })
    }
    return b
}

func (b *RequestBuilder) WithUserPrompt(prompt string) *RequestBuilder {
    b.messages = append(b.messages, Message{
        Role:    "user",
        Content: prompt,
    })
    return b
}

func (b *RequestBuilder) AddUserMessage(content string) *RequestBuilder {
    return b.WithUserPrompt(content)
}

func (b *RequestBuilder) AddSystemMessage(content string) *RequestBuilder {
    return b.WithSystemPrompt(content)
}

func (b *RequestBuilder) AddAssistantMessage(content string) *RequestBuilder {
    b.messages = append(b.messages, Message{
        Role:    "assistant",
        Content: content,
    })
    return b
}

func (b *RequestBuilder) AddMessage(role, content string) *RequestBuilder {
    b.messages = append(b.messages, Message{
        Role:    role,
        Content: content,
    })
    return b
}

func (b *RequestBuilder) AddConversationHistory(history []Message) *RequestBuilder {
    b.messages = append(b.messages, history...)
    return b
}

func (b *RequestBuilder) WithTemperature(t float64) *RequestBuilder {
    if t < 0 || t > 2 {
        panic("temperature must be between 0 and 2")
    }
    b.temperature = &t
    return b
}

func (b *RequestBuilder) WithMaxTokens(tokens int) *RequestBuilder {
    b.maxTokens = &tokens
    return b
}

func (b *RequestBuilder) WithTopP(p float64) *RequestBuilder {
    b.topP = &p
    return b
}

func (b *RequestBuilder) WithFrequencyPenalty(p float64) *RequestBuilder {
    b.frequencyPenalty = &p
    return b
}

func (b *RequestBuilder) WithPresencePenalty(p float64) *RequestBuilder {
    b.presencePenalty = &p
    return b
}

func (b *RequestBuilder) WithStopSequences(sequences []string) *RequestBuilder {
    b.stop = sequences
    return b
}

func (b *RequestBuilder) AddTool(tool Tool) *RequestBuilder {
    b.tools = append(b.tools, tool)
    return b
}

func (b *RequestBuilder) WithToolChoice(choice string) *RequestBuilder {
    b.toolChoice = choice
    return b
}

func (b *RequestBuilder) WithStream(stream bool) *RequestBuilder {
    b.stream = stream
    return b
}

func (b *RequestBuilder) Build() (*Request, error) {
    if len(b.messages) == 0 {
        return nil, errors.New("at least one message is required")
    }

    req := &Request{
        Model:      b.model,
        Messages:   b.messages,
        Stop:       b.stop,
        Tools:      b.tools,
        ToolChoice: b.toolChoice,
        Stream:     b.stream,
    }

    // 只设置非 nil 的可选参数
    if b.temperature != nil {
        req.Temperature = *b.temperature
    }
    if b.maxTokens != nil {
        req.MaxTokens = *b.maxTokens
    }
    if b.topP != nil {
        req.TopP = *b.topP
    }
    if b.frequencyPenalty != nil {
        req.FrequencyPenalty = *b.frequencyPenalty
    }
    if b.presencePenalty != nil {
        req.PresencePenalty = *b.presencePenalty
    }

    return req, nil
}

Client 集成

// 新增方法(不影响现有代码)
func (client *Client) CallWithRequest(req *Request) (string, error) {
    // 使用 req 中的参数发送请求
    // ...
}

总结

核心优势

  1. 灵活性 - 轻松支持复杂场景
  2. 可读性 - 代码自解释,易于理解
  3. 可扩展性 - 添加新功能不破坏现有代码
  4. 类型安全 - 编译时检查,提前发现错误
  5. 向后兼容 - 可以与现有 API 共存

建议

  • 当前阶段: 如果只需要简单对话,现有实现已足够
  • 未来扩展: 当需要以下功能时再引入
    • 多轮对话
    • Function Calling
    • 流式响应
    • 精细参数控制

最佳实践

采用渐进式引入策略:

  1. 保留现有 CallWithMessages API
  2. 新增 CallWithRequest + 构建器
  3. 在文档中推荐新 API但不强制迁移
  4. 根据实际需求逐步完善构建器功能

这样既能保持向后兼容,又能为未来的功能扩展做好准备。