在构建 AI Agent 的浪潮中,让大模型(LLM)具备“行动能力”是核心诉求。对于 Anthropic 的 Claude 模型而言,这种能力被称为 Skill(或 Tool Use)。
本文将基于 Claude 的技术特性,结合 Golang 生态中的 Eino 框架,深入剖析 Skill 的工作原理、Prompt 编写技巧以及工程落地的最佳实践。
第一部分:什么是 Claude Skill?
简单来说,Skill 是给仅能处理文本的 LLM 装上了“手”和“眼”。它允许开发者定义一组外部工具(API、数据库查询、计算器等),并授权 Claude 在遇到特定问题时调用这些工具。
主要应用场景:
- 获取实时信息: 查询股价、天气、最新新闻。
- 执行操作: 发送邮件、在日历添加事件、购买商品。
- 复杂计算: 调用 Wolfram Alpha 或 Python 解释器进行精确计算。
- 企业数据交互: 查询公司内部的 CRM 或 SQL 数据库。
1. 核心原理:预测与中断
Claude 的工具调用并非魔法,其底层逻辑依然基于 “Next Token Prediction” 。整个过程是一个精密的“三明治结构”:
-
感知 (The Brain): 开发者将用户的提问 + 工具清单 (Schema) 一并发给 Claude。例子: “Claude,你有两个工具:
get_weather(city)和send_email(to, body)。用户问:北京天气怎么样?” -
决策 (Reasoning): Claude 分析意图。如果它认为需要使用工具,它不会生成自然语言回复,而是生成一段特定格式的 结构化文本(通常是 XML 或 JSON)。
- Claude:
<tool_use><name>get_weather</name><city>Beijing</city></tool_use>
- Claude:
-
中断与执行: * Claude 暂停 生成。
- 你的代码 (Client) 拦截到这段结构化请求,在后台执行真实的 API 调用。
-
反馈 (Feedback): 你的代码将执行结果(如“25°C”)作为新的输入发回给 Claude。
-
响应 (Response): Claude 结合执行结果,生成最终给用户的自然语言回答。
- Claude 输出: “北京今天的天气不错,气温是 25°C,晴朗。”
关键认知: Claude 自己不运行工具,是你的代码在运行工具。Claude 只是负责“填写参数表单”。
第二部分:Prompt Engineering 的范式转移
在开发 Skill 时,“写 Prompt” 的方式发生了本质变化。你不再需要写长篇大论的指令,而是需要编写精准的 JSON Schema。
工具定义的 description 字段,就是写给 Claude 看的 Prompt。
案例:快递查询工具
❌ 错误的定义
{
"name": "search",
"description": "查物流",
"parameters": { "id": "string" }
}
问题:名字含糊,描述太短,Claude 不知道何时触发,也不知道参数格式。
✅ 正确的定义
{
"name": "get_delivery_status",
"description": "当用户询问包裹位置、发货状态或预计到达时间时使用此工具。",
"input_schema": {
"properties": {
"order_id": {
"type": "string",
"description": "以 'CN' 开头的10位订单编号。如果用户未提供,请礼貌询问。"
}
}
}
}
优势:明确了触发场景(Scenario)和参数约束(Constraint)。
第三部分:Eino 框架实战 (Golang)
在后端开发中,手动拼接上述 JSON 非常繁琐。Eino (CloudWeGo 开源框架) 提供了一套优雅的解决方案:将 Go Struct 自动映射为 Claude Prompt。
1. 定义入参
利用 Go 的 struct tag 来编写 Prompt。
type DeliveryRequest struct {
// `desc` 标签的内容会被 Eino 自动提取并传给 Claude
OrderID string `json:"order_id" desc:"以 'CN' 开头的10位订单编号,例如 CN1234567890"`
}
2. 实现逻辑
编写普通的 Go 函数处理业务。
func GetDeliveryStatus(ctx context.Context, req *DeliveryRequest) (string, error) {
if req.OrderID == "CN123456" {
return "包裹已到达北京转运中心", nil
}
return "", fmt.Errorf("未查询到订单")
}
3. 绑定与自动编排
Eino 负责中间繁琐的序列化、反序列化和路由分发。
// 创建工具
tool, _ := utils.NewTool(&utils.ToolConfig{
Name: "get_delivery_status",
Desc: "查询快递状态工具",
Func: GetDeliveryStatus, // 绑定 Go 函数
})
// 绑定到 Claude 模型
chatModel.BindTools([]*schema.ToolInfo{tool.Info()})
第四部分:构建高质量 Skill 的四大要素
要让 Skill 稳定可靠,仅有基础代码是不够的。以下是经过实战验证的四个关键点:
1. Description 是核心
desc 决定了工具的“触发率”和“准确率”。
- 技巧: 在描述中加入 Negative Prompt(负向提示)。
- 例: “此工具用于查询当前库存。注意:不要用于查询历史销售记录。 ”
2. 字段约束显性化
LLM 不懂隐式规则。
- 技巧: 在 Struct Tag 中加入格式要求和示例(Example) 。
- 例:
desc:"日期,格式必须为 YYYY-MM-DD,如 2024-01-01"。
3. 返回值设计的艺术
后端返回给 Claude 的数据(Tool Output)也是 Prompt 的一部分。
- 技巧: 提高信噪比。不要返回包含大量无用字段的原始 JSON,只返回 Claude 回答问题所需的数据。
4. 错误处理即教学
这是最高级的技巧。当工具调用失败时,不要只返回 Error 500。你应该返回一个 “Teacher Error” (像老师一样的纠错信息),指导 Claude 自我修正。
实战案例:汇率转换器的自动纠错
场景描述:
我们构建一个汇率转换工具。后端只接受标准的 ISO 货币代码(如 CNY, USD),但用户经常习惯说“人民币”或“RMB”。我们将故意不处理前端输入,而是依赖后端报错机制来“教会” Claude。
1. Go 代码实现 (Eino)
func ConvertCurrency(ctx context.Context, req *CurrencyRequest) (string, error) {
validCodes := map[string]bool{"CNY": true, "USD": true}
// 检查:如果 Claude 传了 "RMB",我们要拦截
from := strings.ToUpper(req.FromCurrency)
if !validCodes[from] {
// 【关键点】:不要只返回 "Invalid Currency"。
// 要告诉 Claude 错在哪,以及正确的参考值是什么。
return "", fmt.Errorf(
"API Error: 货币代码 '%s' 无效。请仅使用 ISO 4217 标准代码。" +
"提示:如果你想指人民币,请使用 'CNY';美元请使用 'USD'。请修正参数后重试。",
req.FromCurrency,
)
}
return fmt.Sprintf("转换成功:100 %s = 14.5 %s", from, req.ToCurrency), nil
}
2. 实际运行流程
这就是 “Eino 自动纠错” 发生的过程。用户完全无感知,但后台发生了两次交互。
-
Round 1: 第一次尝试(犯错)
- User: "把 100 RMB 换成美元。"
- Claude 调用:
convert_currency(from="RMB", to="USD") - Eino 返回 (Error):
[Error] API Error: 货币代码 'RMB' 无效...提示:如果你想指人民币,请使用 'CNY'...
-
Round 2: 自我修正(重试)
- Claude 思考: "哎呀,工具报错了。它提示我应该用 'CNY'。好的,我重新调用一次。"
- Claude 再次调用:
convert_currency(from="CNY", to="USD") - Eino 返回 (Success):
转换成功:100 CNY = 14.5 USD
-
最终回复用户: "转换完成!100 人民币大约等于 14.5 美元。"
核心价值:
通过这种机制,你把后端逻辑里的 error 变成了对 AI 的直接指导,增强了 Agent 的韧性(Resilience),避免了直接拒绝用户。
第五部分:进阶与生产环境考量
在技术分享的尾声,为了展示方案的完整性,我们需要讨论 Agent 在生产环境中的复杂挑战。
1. 多步推理与 Graph 编排
现实世界的任务往往不是单一工具能解决的,而是需要 Tool Chaining(工具链) 。
- 场景: 用户问“分析 A 公司的最新财报风险”。
- 流程:
SearchTool(找财报) ->ReaderTool(读 PDF) ->AnalysisTool(提取数据) -> LLM (总结)。 - Eino 优势: 利用 Eino 的
Graph或Chain能力,可以将多个 Skill 串联起来,甚至实现并行的工具调用,而不是简单的一问一答。
代码示例 (Eino Graph 构建):
// 创建一个 Graph
g := compose.NewGraph[string, string]()
// 添加节点:检索器 -> 分析器 -> 总结大模型
g.AddNode("search_tool", searchTool)
g.AddNode("reader_tool", pdfReader)
g.AddNode("summarizer_llm", chatModel)
// 定义边的流向 (Workflow)
g.AddEdge(compose.START, "search_tool")
g.AddEdge("search_tool", "reader_tool")
g.AddEdge("reader_tool", "summarizer_llm")
g.AddEdge("summarizer_llm", compose.END)
// 编译并运行
runnable, _ := g.Compile(ctx)
result, _ := runnable.Invoke(ctx, "分析 Apple 2024 Q3 财报")
2. 安全性:Human-in-the-loop (人机回环)
赋予 AI 行动能力也带来了风险。对于高风险操作(如删除数据、退款转账),必须引入人工确认机制。
-
设计模式:
- Claude 决定调用
refund_user工具。 - Eino 拦截调用请求,暂停 (Suspend) 执行流。
- 系统向管理员发送“确认卡片”。
- 管理员点击“批准”。
- Eino 恢复 (Resume) 执行流,真正运行后端函数。
- Claude 决定调用
代码示例 (带审批的工具逻辑):
func RefundUser(ctx context.Context, req *RefundRequest) (string, error) {
// 1. 检查权限或触发审批流
if req.Amount > 1000 {
// 在实际系统中,这里可能抛出一个特定的 "NeedApprovalError"
// 或者检查 Redis 中的审批 Token 是否存在
if !IsApproved(ctx, req.TransactionID) {
TriggerAdminApproval(req)
return "SYSTEM: 超过大额限制,已发送审批请求给管理员。请管理员批准后再重试此操作。", nil
}
}
// 2. 执行退款
db.ExecuteRefund(req)
return "退款成功", nil
}
3. 可观测性与模型评估
AI 的行为具有不确定性。当 Agent 表现不佳时,我们需要知道是 Prompt 写歪了、工具参数传错了,还是模型本身的能力不足(智力波动) 。因此,除了链路追踪,还需要引入模型能力打分机制。
-
最佳实践: 记录完整的 Trace 并对模型能力进行量化打分。
- Input: 用户到底说了什么?
- Prompt: Eino 最终组装发给 Claude 的 Prompt 长什么样?(包含工具定义)
- Tool Call: Claude 吐出的 JSON 参数是什么?
- Output: 工具返回了什么结果?
- Latency: 工具执行耗时多久?
- Model Score: 记录本次交互中模型所展示的能力得分。可以基于人工反馈(Thumbs up/down)或使用另一个更强的模型(Evaluator)对当前模型的回答逻辑、工具使用准确性进行打分。
代码示例 (Eino Callbacks + Evaluation):
// 创建回调处理器
handler := callbacks.NewHandlerHelper().
// 监听工具调用开始
OnToolStart(func(ctx context.Context, info *schema.ToolInfo, input string) {
log.Printf("[Trace] Tool Start: %s | Input: %s", info.Name, input)
}).
// 监听工具调用结束
OnToolEnd(func(ctx context.Context, info *schema.ToolInfo, output string) {
log.Printf("[Trace] Tool End: %s | Output: %s", info.Name, output)
}).
// 监听 LLM 生成结束
OnModelEnd(func(ctx context.Context, out *schema.ModelOutput, err error) {
log.Printf("[Trace] Model Token Usage: %d", out.Usage.TotalTokens)
// 异步触发模型能力评估
// 既然日志记录了 Input/Output,我们可以异步调用一个 "Judge Model"
// 来给当前 Claude 的表现打分 (0-10分),从而监控模型能力是否下降。
go func() {
score := EvaluateAgentPerformance(out.Content)
metrics.RecordModelScore("claude-3.5-sonnet", score)
}()
}).
Handler()
// 在运行时注入回调
response, _ := chatModel.Generate(
ctx,
userInput,
eino.WithCallbacks(handler), // 注入观察者
)
第六部分: 自我进化
1. 记忆机制模仿:从“无状态”到“无限流”
人脑是连续的,昨天的你会影响今天的你。 而原生 LLM 是无状态的(Stateless),聊完就忘。
在工程上,为了模仿大脑的**“意识流” ,斯坦福大学在著名的“生成式智能体(Generative Agents)”实验中提出了一种“记忆流(Memory Stream)”**架构:
-
感知 (Perception): 记录发生的所有事(Raw Logs)。
-
反思 (Reflection): 并不是所有事都存下来。系统会定期(比如每晚)让 LLM 对今天的日志进行总结,提炼出“高级观点”。
- 原始日志: “我在 8:00 吃了面包”,“我在 8:05 喝了牛奶”。
- 反思提炼: “我喜欢吃西式早餐”。(存入长期记忆)
-
检索 (Retrieval): 当下次做决定时,根据相关性、新近性和重要性去提取记忆。
工程实现: 你可以用图数据库(Graph DB)或者专门的 Agent 记忆框架(如 MemGPT)来实现这种“会做梦、会反思”的类脑系统。
2. 进化机制模仿:在线学习 (Online Learning)
人脑是可塑的(Neuroplasticity),学了新东西,神经元连接就会变。 目前的 LLM 最大的痛点是**“训练完就固化了”**(Static Weights)。
为了在工程上模仿这种可塑性,我们通常采用**“参数微调 + 上下文学习”**的混合架构:
- 快思考 (Fast Weights): 利用 LLM 超长的 Context(如 Kimi 的 20万字),把新学到的知识放在 Prompt 里(In-Context Learning)。这就像人的工作记忆,即学即用,但关机就忘。
- 慢思考 (Slow Weights): 每天晚上,收集白天的高质量数据,用 LoRA(低秩适配)技术快速微调模型的一小部分参数。这就像人睡觉时的记忆巩固,把短期记忆变为长期本能。
实战: 自我进化
第一:实现 Reflection (反思机制)
目标: 让系统在空闲时,或者在对话达到一定轮数后,自动回顾历史,提炼摘要。
在 Eino 中,你可以定义一个双链路 Graph:
- Main Chain: 处理用户实时对话(快)。
- Reflection Chain: 异步处理历史记录(慢)。
架构设计
你可以创建一个 ReflectionNode(反思节点),它本质上是一个特殊的 LLM 调用,Prompt 不是回答问题,而是总结。
Eino 代码实现思路 (Go)
// 1. 定义状态 (State)
type AppState struct {
History []Message // 原始对话历史
Reflections []string // 提炼出的高级观点 (长期记忆)
UserInput string
FinalReply string
}
// 2. 定义反思节点 (Reflection Node)
// 这个节点通常是一个 Lambda,内部调用 LLM 进行总结
reflectionNode := compose.NewLambda(func(ctx context.Context, input *AppState) (*AppState, error) {
// 如果历史记录太短,就不反思
if len(input.History) < 10 {
return input, nil
}
// 构造反思的 Prompt
prompt := "请阅读以下对话历史,提炼出用户的主要偏好、性格特点以及关键事实,作为'高级观点'存入记忆库。"
// ... 将 input.History 拼接到 prompt ...
// 调用 LLM (这里可以使用 Eino 的 Model 组件)
summary, _ := llm.Generate(ctx, prompt)
// 【关键】将新的观点追加到状态中
input.Reflections = append(input.Reflections, summary)
// 清理旧历史 (模拟大脑遗忘机制,只保留最近的 + 总结的)
// input.History = input.History[5:]
return input, nil
})
// 3. 编排 Graph
graph := compose.NewGraph[AppState, AppState]()
graph.AddNode("chat_node", chatNode) // 正常的聊天节点
graph.AddNode("reflection_node", reflectionNode) // 反思节点
// 逻辑:聊完之后,顺便做一次反思 (也可以是异步的)
graph.AddEdge("chat_node", "reflection_node")
graph.AddEdge("reflection_node", compose.END)
// 4. 运行
runnable := graph.Compile()
效果: 用户聊了一整天。系统后台的 Reflections 列表里会增加一条:
"用户偏好使用 Go 语言,对并发编程感兴趣,但在微服务架构设计上存在困惑。" 这就是**“高级观点”**。
第二:实现 Evolution (进化机制)
目标: 让系统“越用越聪明”。 手段: 在 Eino 中,进化主要体现为**“动态 Prompt”或“知识库更新” 。我们不直接修改模型权重(那太慢),我们修改模型上下文**。
这种机制在工程上叫 In-Context Evolution (上下文进化) 。
核心逻辑
当下一轮对话开始时,我们需要把 Reflections 里的内容,注入到 System Prompt 中。
Eino 实现思路
利用 Eino 的 Prompt Template 和变量注入功能。
// 1. 定义动态 Prompt 模板
// 注意 {learned_traits} 占位符
const systemPromptTemplate = `
你是一个智能助手。
关于当前用户的已知画像(不要告诉用户你知道这些):
{learned_traits}
请基于以上画像回答用户的问题。
`
// 2. 定义一个“上下文组装器” (Context Assembler)
assemblerNode := compose.NewLambda(func(ctx context.Context, input *AppState) (*AppState, error) {
// 从数据库或 State 中读取所有的“高级观点”
traits := strings.Join(input.Reflections, "\n")
// 渲染 Prompt
tmpl := prompt.NewTemplate(systemPromptTemplate)
finalPrompt, _ := tmpl.Format(map[string]interface{}{
"learned_traits": traits, // 【关键】这就是进化的体现
})
// 将渲染好的 Prompt 塞给 LLM
input.SystemPrompt = finalPrompt
return input, nil
})
// 3. 将其加入 Graph 的最前端
// Start -> Assembler(注入进化记忆) -> Chat(生成回答) -> Reflector(产生新记忆) -> End
如何模仿“自我优化规则”的进化?
除了记住用户喜好,还可以让 AI 进化自己的工作流。
场景: AI 发现自己老是写出有 Bug 的 SQL 语句。 反思与进化流程:
- Reflection:
ReflectorNode检测到用户报错说 "SQL 跑不通"。 - Analysis: 调用 LLM 分析:“为什么错了?哦,是因为我老是用 SQLite 的语法去查 MySQL。”
- Rule Update (进化) : 系统自动生成一条新规则 —— "注意:当前环境是 MySQL 8.0,严禁使用 SQLite 特有函数" 。
- Storage: 将这条规则存入 "Rule Vector DB" (规则库) 。
- Next Run: 下次写 SQL 前,Retriever 先去规则库查一下“写 SQL 注意事项”,把这条新规则加载进 Prompt。
Eino 架构图解:
graph TD
UserInput --> Retriever["检索器: 查阅'进化后的规则库'"]
Retriever --> Assembler["组装 Prompt"]
Assembler --> LLM["DeepSeek/GPT"]
LLM --> UserOutput
subgraph " 进化回路图 "
UserOutput --> Evaluator["评价器: 用户反馈好不好?"]
Evaluator -->|不好| Reflector["反思节点: 哪里错了?"]
Reflector -->|提炼新规则| RuleDB[("规则向量库")]
RuleDB -.->|更新| Retriever
end
总结
在 Eino 和 Claude 的体系下开发 Skill,本质上是在进行一场跨语言的沟通:你用 Go 代码定义规则,Eino 将其翻译为 JSON Schema,Claude 据此进行推理,最后通过你的 Go 函数与现实世界交互。
记住一条原则:你的 Go Struct 定义、函数返回值、甚至错误信息,全都是在跟 AI 对话。 写得越清楚,AI 表现就越智能。