Go 调用 Claude API 实战——流式输出、工具调用、多轮对话完整实现
Go 调用 Claude API 实战——流式输出、工具调用、多轮对话完整实现
适读人群:有 Go 基础、想接入 Anthropic Claude API 的开发者 | 阅读时长:约 18 分钟 | 核心价值:从零到生产可用的 Claude Go 客户端,包含流式、工具调用、多轮对话三个核心场景
去年底,一个做保险核心系统的团队找到我,他们想在理赔审核流程里加一个 AI 辅助决策环节。需求说起来不复杂:上传理赔材料,AI 读完后给出初审意见,标注哪些条款有疑问。但他们后端全是 Java,新业务打算用 Go 重写,AI 这块自然也要用 Go 搞定。
我花了一周时间帮他们把 Claude API 的 Go 集成跑通,踩了不少坑。这篇文章就把这些坑和解法完整记下来。
为什么选 Claude 而不是 GPT-4
这个问题在项目开始的时候争了两天。最后选 Claude 的原因很实际:
- 200K context window:保险理赔文件动辄几十页,GPT-4 的 128K 经常不够用,Claude 3 的 200K 对这类场景有明显优势
- 工具调用更稳定:我在 GPT-4 上遇到过工具参数随机缺字段的问题,Claude 这块表现更可预期
- 成本:claude-3-haiku 在简单分类任务上比 gpt-3.5-turbo 便宜,claude-3-sonnet 的性价比也不错
当然 Claude 也有缺点,后面会说。
项目结构
claude-go-demo/
├── main.go
├── client/
│ ├── claude.go # 核心客户端
│ ├── stream.go # 流式处理
│ └── tools.go # 工具调用
├── conversation/
│ └── history.go # 多轮对话管理
└── go.mod依赖:
go get github.com/anthropics/anthropic-sdk-goAnthropic 的官方 Go SDK 在 2024 年才正式发布,之前社区有几个非官方的,坑比较多。现在直接用官方的就行。
基础调用:先把 Hello World 跑起来
package main
import (
"context"
"fmt"
"os"
anthropic "github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
func main() {
client := anthropic.NewClient(
option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")),
)
ctx := context.Background()
msg, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.F(anthropic.ModelClaude3_5SonnetLatest),
MaxTokens: anthropic.F(int64(1024)),
Messages: anthropic.F([]anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("你好,请用一句话介绍自己")),
}),
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Println(msg.Content[0].(anthropic.TextBlock).Text)
}这段代码能跑起来之后,基础就有了。注意几个细节:
anthropic.F()是泛型包装函数,把值包成anthropic.Field[T],这是该 SDK 的设计模式MaxTokens是必填的,不填会报错- 响应的
Content是接口切片,需要类型断言
流式输出:让用户感知 AI 在"思考"
最开始那个保险项目,产品经理坚持要流式输出。原因很朴实——他之前用过 ChatGPT,觉得字一个一个蹦出来"有感觉",如果等 10 秒然后一堆文字闪出来,用户会以为卡了。
流式输出的代码稍微复杂一些:
package client
import (
"context"
"fmt"
"os"
anthropic "github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
)
type ClaudeClient struct {
client *anthropic.Client
model anthropic.Model
}
func NewClaudeClient(apiKey string) *ClaudeClient {
c := anthropic.NewClient(option.WithAPIKey(apiKey))
return &ClaudeClient{
client: c,
model: anthropic.ModelClaude3_5SonnetLatest,
}
}
// StreamChat 流式对话,通过 callback 实时回调文本片段
func (c *ClaudeClient) StreamChat(ctx context.Context, prompt string, onChunk func(string)) (string, error) {
stream := c.client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
Model: anthropic.F(c.model),
MaxTokens: anthropic.F(int64(2048)),
Messages: anthropic.F([]anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
}),
})
var fullText string
for stream.Next() {
event := stream.Current()
switch e := event.AsUnion().(type) {
case anthropic.ContentBlockDeltaEvent:
if delta, ok := e.Delta.AsUnion().(anthropic.TextDelta); ok {
onChunk(delta.Text)
fullText += delta.Text
}
}
}
if err := stream.Err(); err != nil {
return fullText, fmt.Errorf("stream error: %w", err)
}
return fullText, nil
}
// 使用示例
func ExampleStream() {
c := NewClaudeClient(os.Getenv("ANTHROPIC_API_KEY"))
_, err := c.StreamChat(context.Background(),
"分析以下保险条款中的免责事项:...",
func(chunk string) {
fmt.Print(chunk) // 实时打印到终端
},
)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
fmt.Println() // 换行
}踩坑 1:流式连接中断不报错
现象:跑了一段时间发现有些请求莫名其妙就没有输出了,既不报错,也没有完整的结果。
原因:流式连接在网络不稳定时会静默断开,stream.Next() 直接返回 false,stream.Err() 却是 nil。
解法:检查最终输出长度,如果明显偏短,触发重试;同时设置合理的超时上下文:
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()另外,流式模式下要特别注意 context 取消的处理,用户关闭连接时要能及时停下来,不然 goroutine 泄漏。
工具调用:让 Claude 调用你的业务函数
工具调用(Function Calling / Tool Use)是我觉得 Claude API 里最有意思的特性。它让 LLM 从"聊天机器人"变成了能干活的"代理"。
在保险这个场景里,我们定义了几个工具:查询保单信息、查询历史理赔记录、触发人工审核流程。
package client
import (
"context"
"encoding/json"
"fmt"
anthropic "github.com/anthropics/anthropic-sdk-go"
)
// 定义工具
var claimTools = []anthropic.ToolParam{
{
Name: anthropic.F("query_policy"),
Description: anthropic.F("根据保单号查询保单信息,包括险种、保额、生效日期等"),
InputSchema: anthropic.F(anthropic.ToolInputSchemaParam{
Type: anthropic.F(anthropic.ToolInputSchemaTypeObject),
Properties: anthropic.F(map[string]interface{}{
"policy_id": map[string]interface{}{
"type": "string",
"description": "保单号",
},
}),
Required: anthropic.F([]string{"policy_id"}),
}),
},
{
Name: anthropic.F("query_claim_history"),
Description: anthropic.F("查询被保人的历史理赔记录"),
InputSchema: anthropic.F(anthropic.ToolInputSchemaParam{
Type: anthropic.F(anthropic.ToolInputSchemaTypeObject),
Properties: anthropic.F(map[string]interface{}{
"insured_id": map[string]interface{}{
"type": "string",
"description": "被保人身份证号(脱敏后)",
},
}),
Required: anthropic.F([]string{"insured_id"}),
}),
},
}
// 模拟工具执行函数
func executeTool(toolName string, input map[string]interface{}) string {
switch toolName {
case "query_policy":
policyID := input["policy_id"].(string)
// 实际项目中这里调用数据库
return fmt.Sprintf(`{"policy_id": "%s", "type": "意外险", "amount": 500000, "status": "有效"}`, policyID)
case "query_claim_history":
insuredID := input["insured_id"].(string)
return fmt.Sprintf(`{"insured_id": "%s", "total_claims": 2, "last_claim_date": "2023-06-15", "total_amount": 15000}`, insuredID)
default:
return `{"error": "unknown tool"}`
}
}
// ToolChat 支持工具调用的对话(自动循环直到 AI 给出最终答案)
func (c *ClaudeClient) ToolChat(ctx context.Context, userMessage string) (string, error) {
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(userMessage)),
}
for {
resp, err := c.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.F(c.model),
MaxTokens: anthropic.F(int64(4096)),
Tools: anthropic.F(claimTools),
Messages: anthropic.F(messages),
})
if err != nil {
return "", fmt.Errorf("API call failed: %w", err)
}
// 如果停止原因是 end_turn,说明 AI 给出了最终答案
if resp.StopReason == anthropic.MessageStopReasonEndTurn {
for _, block := range resp.Content {
if tb, ok := block.(anthropic.TextBlock); ok {
return tb.Text, nil
}
}
}
// 停止原因是 tool_use,需要执行工具
if resp.StopReason == anthropic.MessageStopReasonToolUse {
// 把 AI 的响应加入消息历史
messages = append(messages, anthropic.NewAssistantMessage(resp.Content...))
// 执行所有工具调用
var toolResults []anthropic.MessageParamContentUnion
for _, block := range resp.Content {
toolUse, ok := block.(anthropic.ToolUseBlock)
if !ok {
continue
}
// 解析工具输入
var input map[string]interface{}
if err := json.Unmarshal(toolUse.Input, &input); err != nil {
return "", fmt.Errorf("failed to parse tool input: %w", err)
}
// 执行工具
result := executeTool(toolUse.Name, input)
// 构造工具结果
toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, result, false))
}
// 把工具结果加入消息历史
messages = append(messages, anthropic.NewUserMessage(toolResults...))
}
}
}踩坑 2:工具调用死循环
现象:有一次工具函数返回了一个错误 JSON,AI 不断重试同一个工具,陷入无限循环,跑了几百轮才超时,白白浪费 token。
原因:没有设置最大循环次数,也没有处理工具返回错误的情况。
解法:加循环计数器,超过阈值直接返回错误;工具函数返回错误时,在结果里明确标注 "error": "具体原因",让 AI 知道该放弃了:
const maxToolRounds = 10
round := 0
for {
if round >= maxToolRounds {
return "", fmt.Errorf("exceeded max tool call rounds (%d)", maxToolRounds)
}
round++
// ...
}多轮对话:维护会话上下文
多轮对话的本质就是把历史消息带进每次请求。Claude API 是无状态的,上下文靠你自己管理。
package conversation
import (
"context"
"fmt"
"sync"
anthropic "github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"os"
)
// ConversationHistory 线程安全的对话历史
type ConversationHistory struct {
mu sync.RWMutex
messages []anthropic.MessageParam
client *anthropic.Client
model anthropic.Model
system string // 系统提示词
}
func NewConversation(systemPrompt string) *ConversationHistory {
return &ConversationHistory{
client: anthropic.NewClient(option.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY"))),
model: anthropic.ModelClaude3_5SonnetLatest,
system: systemPrompt,
}
}
func (ch *ConversationHistory) Chat(ctx context.Context, userInput string) (string, error) {
ch.mu.Lock()
// 追加用户消息
ch.messages = append(ch.messages, anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)))
messages := make([]anthropic.MessageParam, len(ch.messages))
copy(messages, ch.messages)
ch.mu.Unlock()
params := anthropic.MessageNewParams{
Model: anthropic.F(ch.model),
MaxTokens: anthropic.F(int64(2048)),
Messages: anthropic.F(messages),
}
if ch.system != "" {
params.System = anthropic.F([]anthropic.TextBlockParam{
{Type: anthropic.F(anthropic.TextBlockParamTypeText), Text: anthropic.F(ch.system)},
})
}
resp, err := ch.client.Messages.New(ctx, params)
if err != nil {
// 失败时要把刚加入的用户消息回滚
ch.mu.Lock()
ch.messages = ch.messages[:len(ch.messages)-1]
ch.mu.Unlock()
return "", fmt.Errorf("API error: %w", err)
}
var assistantText string
for _, block := range resp.Content {
if tb, ok := block.(anthropic.TextBlock); ok {
assistantText += tb.Text
}
}
// 把 AI 回复也加入历史
ch.mu.Lock()
ch.messages = append(ch.messages, anthropic.NewAssistantMessage(resp.Content...))
ch.mu.Unlock()
return assistantText, nil
}
// TrimHistory 当历史消息过多时,保留最近 N 轮
func (ch *ConversationHistory) TrimHistory(keepRounds int) {
ch.mu.Lock()
defer ch.mu.Unlock()
keepMessages := keepRounds * 2 // 每轮包含用户+AI两条消息
if len(ch.messages) > keepMessages {
ch.messages = ch.messages[len(ch.messages)-keepMessages:]
}
}
// TokenEstimate 粗估当前历史消耗的 token 数(按字符数/4估算)
func (ch *ConversationHistory) TokenEstimate() int {
ch.mu.RLock()
defer ch.mu.RUnlock()
total := 0
for _, msg := range ch.messages {
for _, block := range msg.Content {
// 粗略估算:4个字符约1个token
if tb, ok := block.(interface{ GetText() string }); ok {
total += len(tb.GetText()) / 4
}
}
}
return total
}踩坑 3:context window 悄悄超限
现象:对话到某个轮次之后,突然报 context_length_exceeded 错误,而且这个错误在本地测试时从没出现过,上了生产才碰到。
原因:本地测试的对话比较短,没注意到历史消息会累积。保险理赔的分析文本很长,十几轮下来就把 200K 塞满了。
解法:
- 用
resp.Usage获取实际 token 消耗,每轮对话后记录 - 超过阈值(比如 150K tokens)时自动触发
TrimHistory - 或者用摘要压缩:让 AI 把早期对话总结成几百字,替换掉原始消息
// 每次对话后检查 token 用量
fmt.Printf("本轮消耗: input=%d output=%d tokens\n", resp.Usage.InputTokens, resp.Usage.OutputTokens)成本控制:API 费用怎么算
以 claude-3-5-sonnet-20241022 为例(2024 年底价格):
- 输入:$3 / 1M tokens
- 输出:$15 / 1M tokens
保险理赔场景的实测数据:
- 每次分析请求,平均输入约 8000 tokens(文件内容 + 历史 + 系统提示)
- 平均输出约 1200 tokens
- 成本 = 8000 × $3/1M + 1200 × $15/1M = $0.024 + $0.018 = 约 $0.042 每次请求
- 按每天处理 500 份理赔材料估算:$21 / 天,$630 / 月
如果换成 claude-3-haiku 做初筛,只有复杂案例才上 sonnet:
- haiku:输入 $0.25/1M,输出 $1.25/1M
- 初筛成本约 $0.002 每次
- 按 80% 案例能被 haiku 过滤,月成本可以降到 $150 以下
这个两级策略是我目前在实际项目里用得最多的成本优化方案。
生产环境注意事项
重试和限流:Anthropic API 有速率限制,遇到 429 要指数退避重试:
import "github.com/cenkalti/backoff/v4"
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 2 * time.Minute
err := backoff.Retry(func() error {
_, err := client.Messages.New(ctx, params)
var apiErr *anthropic.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
return err // 可重试
}
return backoff.Permanent(err) // 不可重试
}, bo)API Key 安全:绝对不要把 key 写进代码或配置文件提交到 git,用环境变量或 secrets manager。
Prompt 注入防护:如果用户输入会直接拼入 prompt,要做输入过滤,防止 "ignore previous instructions" 这类攻击。
选型建议
- 开发调试:用 claude-3-haiku,速度快,成本低
- 生产功能:claude-3-5-sonnet,性价比最好
- 超长文档:才用 claude-3-opus 或需要 200K 上下文时
- 工具调用场景:Sonnet 比 Haiku 稳定很多,复杂工具链别用 Haiku
SDK 版本:截至写这篇文章,官方 Go SDK v0.2.x 已经基本稳定,生产可用。
