一、为什么需要统一网关
想象一下你的业务接了三个大模型:
业务代码中直接调用:
┌──────────────────────────────────────────────┐
│ func askAI(question string) string { │
│ if model == "openai" { │
│ // openai 的请求格式 │
│ } else if model == "claude" { │
│ // claude 的请求格式 │
│ } else if model == "wenxin" { │
│ // 文心一言的请求格式 │
│ } │
│ } │
└──────────────────────────────────────────────┘
痛点:
1. 每个模型 SDK 不同、请求格式不同、错误码不同
2. 切换模型要改业务代码
3. 无法统一做限流、鉴权、降级
4. 新接一个模型要改所有调用方
统一网关的目标:对外暴露一套标准接口,对内屏蔽各厂商差异。
二、架构设计
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 业务服务A │ │ 业务服务B │ │ 业务服务C │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└────────────────┼────────────────┘
│
┌────────▼────────┐
│ LLM API 网关 │
│ │
│ 统一请求/响应格式 │
│ 鉴权 / 限流 │
│ 模型路由 / 降级 │
│ Token 统计 │
└───┬────┬────┬───┘
│ │ │
┌────────┘ │ └────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ OpenAI │ │ Claude │ │ 文心一言 │
└──────────┘ └──────────┘ └──────────┘
三、统一请求/响应模型
// llm/gateway/model.go
package gateway
import (
"time"
)
// ChatRequest 统一请求格式
type ChatRequest struct {
Model string `json:"model"` // 模型标识:gpt-4/claude-3/wenxin-4
Messages []ChatMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []Tool `json:"tools,omitempty"`
}
// ChatMessage 统一消息格式
type ChatMessage struct {
Role string `json:"role"` // system/user/assistant/tool
Content string `json:"content"`
Name string `json:"name,omitempty"`
}
// ChatResponse 统一响应格式
type ChatResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []Choice `json:"choices"`
Usage TokenUsage `json:"usage"`
CreatedAt time.Time `json:"created_at"`
}
// Choice 选项
type Choice struct {
Index int `json:"index"`
Message ChatMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
// TokenUsage Token 用量
type TokenUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
// Tool 工具定义
type Tool struct {
Type string `json:"type"` // function
Function ToolFunction `json:"function"`
}
type ToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters interface{} `json:"parameters"`
}
四、适配器模式实现
// llm/gateway/adapter.go
package gateway
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// LLMAdapter 模型适配器接口
type LLMAdapter interface {
Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error)
Name() string
}
// ─── OpenAI 适配器 ───
type OpenAIAdapter struct {
apiKey string
baseURL string
client *http.Client
}
func NewOpenAIAdapter(apiKey string) *OpenAIAdapter {
return &OpenAIAdapter{
apiKey: apiKey,
baseURL: "https://api.openai.com/v1",
client: &http.Client{Timeout: 60 * time.Second},
}
}
func (a *OpenAIAdapter) Name() string {
return "openai"
}
func (a *OpenAIAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
// 转换请求格式
openAIReq := a.toOpenAIRequest(req)
data, _ := json.Marshal(openAIReq)
httpReq, _ := http.NewRequestWithContext(ctx, "POST",
a.baseURL+"/chat/completions",
strings.NewReader(string(data)))
httpReq.Header.Set("Authorization", "Bearer "+a.apiKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := a.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("openai request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("openai error %d: %s", resp.StatusCode, string(body))
}
// 解析 OpenAI 响应
var openAIResp openAIChatResponse
if err := json.Unmarshal(body, &openAIResp); err != nil {
return nil, fmt.Errorf("parse openai response: %w", err)
}
return a.toUnifiedResponse(&openAIResp), nil
}
func (a *OpenAIAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
// Streaming 实现(下讲详细展开)
ch := make(chan ChatResponse)
go func() {
defer close(ch)
// SSE 解析逻辑
}()
return ch, nil
}
// OpenAI 原始请求结构
type openAIChatRequest struct {
Model string `json:"model"`
Messages []openAIMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
Tools []openAITool `json:"tools,omitempty"`
}
type openAIMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type openAITool struct {
Type string `json:"type"`
Function openAIToolFunction `json:"function"`
}
type openAIToolFunction struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters interface{} `json:"parameters"`
}
type openAIChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Model string `json:"model"`
Choices []openAIChoice `json:"choices"`
Usage openAIUsage `json:"usage"`
}
type openAIChoice struct {
Index int `json:"index"`
Message openAIMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
type openAIUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
func (a *OpenAIAdapter) toOpenAIRequest(req *ChatRequest) *openAIChatRequest {
messages := make([]openAIMessage, len(req.Messages))
for i, m := range req.Messages {
messages[i] = openAIMessage{Role: m.Role, Content: m.Content}
}
tools := make([]openAITool, len(req.Tools))
for i, t := range req.Tools {
tools[i] = openAITool{
Type: t.Type,
Function: openAIToolFunction{
Name: t.Function.Name,
Description: t.Function.Description,
Parameters: t.Function.Parameters,
},
}
}
return &openAIChatRequest{
Model: req.Model,
Messages: messages,
Temperature: req.Temperature,
MaxTokens: req.MaxTokens,
Stream: req.Stream,
Tools: tools,
}
}
func (a *OpenAIAdapter) toUnifiedResponse(openAIResp *openAIChatResponse) *ChatResponse {
choices := make([]Choice, len(openAIResp.Choices))
for i, c := range openAIResp.Choices {
choices[i] = Choice{
Index: c.Index,
Message: ChatMessage{
Role: c.Message.Role,
Content: c.Message.Content,
},
FinishReason: c.FinishReason,
}
}
return &ChatResponse{
ID: openAIResp.ID,
Model: openAIResp.Model,
Choices: choices,
Usage: TokenUsage{
PromptTokens: openAIResp.Usage.PromptTokens,
CompletionTokens: openAIResp.Usage.CompletionTokens,
TotalTokens: openAIResp.Usage.TotalTokens,
},
CreatedAt: time.Now(),
}
}
// ─── Claude 适配器 ───
type ClaudeAdapter struct {
apiKey string
baseURL string
client *http.Client
}
func NewClaudeAdapter(apiKey string) *ClaudeAdapter {
return &ClaudeAdapter{
apiKey: apiKey,
baseURL: "https://api.anthropic.com/v1",
client: &http.Client{Timeout: 60 * time.Second},
}
}
func (a *ClaudeAdapter) Name() string {
return "claude"
}
func (a *ClaudeAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
// Claude 的消息格式不同:system 单独传,messages 只有 user/assistant
claudeReq := a.toClaudeRequest(req)
data, _ := json.Marshal(claudeReq)
httpReq, _ := http.NewRequestWithContext(ctx, "POST",
a.baseURL+"/messages",
strings.NewReader(string(data)))
httpReq.Header.Set("x-api-key", a.apiKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
httpReq.Header.Set("Content-Type", "application/json")
resp, err := a.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("claude request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("claude error %d: %s", resp.StatusCode, string(body))
}
var claudeResp claudeResponse
if err := json.Unmarshal(body, &claudeResp); err != nil {
return nil, fmt.Errorf("parse claude response: %w", err)
}
return a.toUnifiedResponse(&claudeResp), nil
}
func (a *ClaudeAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
ch := make(chan ChatResponse)
go func() {
defer close(ch)
}()
return ch, nil
}
// Claude 请求/响应结构
type claudeRequest struct {
Model string `json:"model"`
Messages []claudeMessage `json:"messages"`
System string `json:"system,omitempty"`
MaxTokens int `json:"max_tokens"`
Temperature float64 `json:"temperature,omitempty"`
}
type claudeMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type claudeResponse struct {
ID string `json:"id"`
Type string `json:"type"`
Role string `json:"role"`
Content []claudeContent `json:"content"`
Model string `json:"model"`
Usage claudeUsage `json:"usage"`
}
type claudeContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
type claudeUsage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
}
func (a *ClaudeAdapter) toClaudeRequest(req *ChatRequest) *claudeRequest {
var system string
messages := make([]claudeMessage, 0)
for _, m := range req.Messages {
if m.Role == "system" {
system = m.Content
} else {
messages = append(messages, claudeMessage{Role: m.Role, Content: m.Content})
}
}
maxTokens := req.MaxTokens
if maxTokens == 0 {
maxTokens = 1024
}
return &claudeRequest{
Model: req.Model,
Messages: messages,
System: system,
MaxTokens: maxTokens,
Temperature: req.Temperature,
}
}
func (a *ClaudeAdapter) toUnifiedResponse(claudeResp *claudeResponse) *ChatResponse {
content := ""
if len(claudeResp.Content) > 0 {
content = claudeResp.Content[0].Text
}
return &ChatResponse{
ID: claudeResp.ID,
Model: claudeResp.Model,
Choices: []Choice{
{
Index: 0,
Message: ChatMessage{Role: "assistant", Content: content},
FinishReason: "stop",
},
},
Usage: TokenUsage{
PromptTokens: claudeResp.Usage.InputTokens,
CompletionTokens: claudeResp.Usage.OutputTokens,
TotalTokens: claudeResp.Usage.InputTokens + claudeResp.Usage.OutputTokens,
},
CreatedAt: time.Now(),
}
}
// ─── 文心一言适配器 ───
type WenxinAdapter struct {
apiKey string
secretKey string
client *http.Client
token string
}
func NewWenxinAdapter(apiKey, secretKey string) *WenxinAdapter {
return &WenxinAdapter{
apiKey: apiKey,
secretKey: secretKey,
client: &http.Client{Timeout: 60 * time.Second},
}
}
func (a *WenxinAdapter) Name() string {
return "wenxin"
}
func (a *WenxinAdapter) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
// 先获取 access_token
if err := a.refreshToken(ctx); err != nil {
return nil, err
}
wenxinReq := a.toWenxinRequest(req)
data, _ := json.Marshal(wenxinReq)
url := fmt.Sprintf("https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=%s", a.token)
httpReq, _ := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(data)))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := a.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("wenxin request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("wenxin error %d: %s", resp.StatusCode, string(body))
}
var wenxinResp wenxinResponse
if err := json.Unmarshal(body, &wenxinResp); err != nil {
return nil, fmt.Errorf("parse wenxin response: %w", err)
}
return a.toUnifiedResponse(&wenxinResp), nil
}
func (a *WenxinAdapter) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
ch := make(chan ChatResponse)
go func() {
defer close(ch)
}()
return ch, nil
}
// 文心一言请求/响应结构
type wenxinRequest struct {
Messages []wenxinMessage `json:"messages"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
}
type wenxinMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type wenxinResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Result string `json:"result"`
IsTruncated bool `json:"is_truncated"`
NeedClearHistory bool `json:"need_clear_history"`
Usage wenxinUsage `json:"usage"`
}
type wenxinUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
func (a *WenxinAdapter) refreshToken(ctx context.Context) error {
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s",
a.apiKey, a.secretKey)
resp, err := a.client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
var result struct {
AccessToken string `json:"access_token"`
}
json.NewDecoder(resp.Body).Decode(&result)
a.token = result.AccessToken
return nil
}
func (a *WenxinAdapter) toWenxinRequest(req *ChatRequest) *wenxinRequest {
messages := make([]wenxinMessage, len(req.Messages))
for i, m := range req.Messages {
messages[i] = wenxinMessage{Role: m.Role, Content: m.Content}
}
return &wenxinRequest{
Messages: messages,
Temperature: req.Temperature,
}
}
func (a *WenxinAdapter) toUnifiedResponse(wenxinResp *wenxinResponse) *ChatResponse {
return &ChatResponse{
ID: wenxinResp.ID,
Model: "wenxin",
Choices: []Choice{
{
Index: 0,
Message: ChatMessage{Role: "assistant", Content: wenxinResp.Result},
FinishReason: "stop",
},
},
Usage: TokenUsage{
PromptTokens: wenxinResp.Usage.PromptTokens,
CompletionTokens: wenxinResp.Usage.CompletionTokens,
TotalTokens: wenxinResp.Usage.TotalTokens,
},
CreatedAt: time.Now(),
}
}
五、网关核心实现
// llm/gateway/gateway.go
package gateway
import (
"context"
"fmt"
"log"
"sync"
"time"
"golang.org/x/time/rate"
)
// LLMGateway LLM API 网关
type LLMGateway struct {
adapters map[string]LLMAdapter
limiter *rate.Limiter
stats *GatewayStats
mu sync.RWMutex
}
// GatewayStats 网关统计
type GatewayStats struct {
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
Errors map[string]int64 `json:"errors"`
AvgLatencyMs float64 `json:"avg_latency_ms"`
lastReset time.Time
mu sync.Mutex
}
// GatewayOption 网关配置
type GatewayOption struct {
QPSLimit float64
BurstLimit int
}
// NewLLMGateway 创建网关
func NewLLMGateway(opts GatewayOption) *LLMGateway {
limiter := rate.NewLimiter(rate.Limit(opts.QPSLimit), opts.BurstLimit)
return &LLMGateway{
adapters: make(map[string]LLMAdapter),
limiter: limiter,
stats: &GatewayStats{
Errors: make(map[string]int64),
lastReset: time.Now(),
},
}
}
// RegisterAdapter 注册模型适配器
func (g *LLMGateway) RegisterAdapter(adapter LLMAdapter) {
g.mu.Lock()
defer g.mu.Unlock()
g.adapters[adapter.Name()] = adapter
log.Printf("[Gateway] Registered adapter: %s", adapter.Name())
}
// Chat 统一聊天接口
func (g *LLMGateway) Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error) {
start := time.Now()
// 1. 限流检查
if err := g.limiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("rate limit: %w", err)
}
// 2. 查找适配器
adapter, err := g.getAdapter(req.Model)
if err != nil {
return nil, err
}
// 3. 调用模型
resp, err := adapter.Chat(ctx, req)
// 4. 记录统计
latency := time.Since(start)
g.recordStats(adapter.Name(), resp, err, latency)
return resp, err
}
// ChatStream 统一流式接口
func (g *LLMGateway) ChatStream(ctx context.Context, req *ChatRequest) (<-chan ChatResponse, error) {
adapter, err := g.getAdapter(req.Model)
if err != nil {
return nil, err
}
req.Stream = true
return adapter.ChatStream(ctx, req)
}
// GetStats 获取统计信息
func (g *LLMGateway) GetStats() *GatewayStats {
g.stats.mu.Lock()
defer g.stats.mu.Unlock()
return g.stats
}
// getAdapter 获取对应模型的适配器
func (g *LLMGateway) getAdapter(model string) (LLMAdapter, error) {
g.mu.RLock()
defer g.mu.RUnlock()
// 根据模型名推断厂商
provider := detectProvider(model)
adapter, ok := g.adapters[provider]
if !ok {
return nil, fmt.Errorf("unsupported model: %s (provider: %s)", model, provider)
}
return adapter, nil
}
// detectProvider 根据模型名推断厂商
func detectProvider(model string) string {
if model == "" {
return "openai"
}
switch {
case containsAny(model, "gpt", "o1", "o3"):
return "openai"
case containsAny(model, "claude"):
return "claude"
case containsAny(model, "ernie", "wenxin"):
return "wenxin"
default:
return "openai" // 默认走 OpenAI 格式
}
}
func containsAny(s string, substrs ...string) bool {
for _, sub := range substrs {
if len(sub) <= len(s) {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
}
}
return false
}
// recordStats 记录调用统计
func (g *LLMGateway) recordStats(provider string, resp *ChatResponse, err error, latency time.Duration) {
g.stats.mu.Lock()
defer g.stats.mu.Unlock()
g.stats.TotalRequests++
if err != nil {
g.stats.Errors[provider]++
return
}
if resp != nil {
g.stats.TotalTokens += int64(resp.Usage.TotalTokens)
}
// 滑动平均延迟
avg := g.stats.AvgLatencyMs
count := float64(g.stats.TotalRequests)
g.stats.AvgLatencyMs = avg + (float64(latency.Milliseconds())-avg)/count
}
六、HTTP 服务与使用示例
// llm/gateway/server.go
package gateway
import (
"encoding/json"
"log"
"net/http"
)
// HTTPServer 网关 HTTP 服务
type HTTPServer struct {
gateway *LLMGateway
}
func NewHTTPServer(gateway *LLMGateway) *HTTPServer {
return &HTTPServer{gateway: gateway}
}
// HandleChat 聊天接口
func (s *HTTPServer) HandleChat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req ChatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := s.gateway.Chat(r.Context(), &req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
// HandleStats 统计接口
func (s *HTTPServer) HandleStats(w http.ResponseWriter, r *http.Request) {
stats := s.gateway.GetStats()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stats)
}
// Run 启动 HTTP 服务
func (s *HTTPServer) Run(addr string) error {
mux := http.NewServeMux()
mux.HandleFunc("/v1/chat/completions", s.HandleChat)
mux.HandleFunc("/stats", s.HandleStats)
log.Printf("[Gateway] Server starting on %s", addr)
return http.ListenAndServe(addr, mux)
}
// ─── 使用示例 ───
func Example() {
// 1. 创建网关
gateway := NewLLMGateway(GatewayOption{
QPSLimit: 100,
BurstLimit: 20,
})
// 2. 注册适配器
gateway.RegisterAdapter(NewOpenAIAdapter("sk-your-openai-key"))
gateway.RegisterAdapter(NewClaudeAdapter("sk-ant-your-claude-key"))
gateway.RegisterAdapter(NewWenxinAdapter("your-api-key", "your-secret-key"))
// 3. 启动 HTTP 服务
server := NewHTTPServer(gateway)
go server.Run(":8080")
// 4. 业务方调用(只需知道一个地址)
// curl -X POST http://localhost:8080/v1/chat/completions \
// -H 'Content-Type: application/json' \
// -d '{
// "model": "gpt-4",
// "messages": [{"role": "user", "content": "你好"}]
// }'
// 5. 切换模型只需改 model 字段
// curl -X POST http://localhost:8080/v1/chat/completions \
// -d '{"model": "claude-3-opus", "messages": [...]}'
}
七、生产坑位
| # | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | API Key 泄露 | Key 被窃取导致巨额账单 | 网关统一管理 Key,业务方无感知 |
| 2 | 模型限流 | 429 Too Many Requests | 网关层排队 + 退避重试 |
| 3 | 超时未处理 | 请求卡死导致 goroutine 泄漏 | 统一超时控制(context deadline) |
| 4 | Token 统计不准 | 账单对不上 | 网关统一计数 + 定期对账 |
| 5 | 模型降级缺失 | 主力模型挂了,业务全挂 | 配置 fallback 模型列表 |
| 6 | 流式中断 | 客户端断连后服务器还在推 | 检测 ctx.Done() 及时清理 |
八、本讲小结
┌─────────────────────────────────────────────────────────────┐
│ 第2讲 核心收获 │
│ │
│ ✅ 理解了 LLM API 网关的必要性 │
│ ✅ 掌握了适配器模式屏蔽多厂商差异 │
│ ✅ 实现了 OpenAI / Claude / 文心一言 的适配器 │
│ ✅ 实现了统一限流、统计、路由 │
│ ✅ 提供了统一的 HTTP 接口供业务方调用 │
│ │
│ 核心设计模式:适配器模式 │
│ 核心原则:业务代码不改,换模型只改 model 字段 │
└─────────────────────────────────────────────────────────────┘
九、配套工具推荐
🧰 开发之余的小工具推荐
在调试 LLM API 网关时,经常需要对比不同模型的返回结果、清洗 JSON 响应、生成测试用的 RequestID:
- JSON 格式化:zz365.top 的 JSON 工具可以把大模型返回的嵌套 JSON 一键展开,快速定位字段路径
- UUID 生成器:给每次 API 调用分配唯一 ID,方便链路追踪和排障
- Base64 编解码:处理某些模型返回的 Base64 编码内容
- 文本差异对比:对比 GPT-4 和 Claude 对同一 Prompt 的输出差异
同类可备站点:
提醒:所有涉及 API Key 和业务数据的操作,务必先在本地脱敏再使用在线工具。
下一讲预告:《Prompt 工程与管理------像管理代码一样管理提示词》
你会学到:Prompt 模板化、版本管理、A/B 测试、变量注入、Prompt 效果评估。