用 Claude Opus 4.7 做 AI Agent:从零搭建一个能自主调用工具的智能体(2026 实战)

4 阅读1分钟

上个月我接了个私活,甲方要做一个"能自己查天气、查数据库、发邮件"的客服机器人。说白了就是个 AI Agent。一开始我用 GPT-5.5 的 function calling 搞了个原型,效果还行但推理链经常断——模型会莫名其妙跳过某个工具调用步骤。后来换成 Claude Opus 4.7 试了下,extended thinking 配合 tool_use 的效果好得离谱,复杂任务的完成率从 68% 直接拉到 91%。

这篇文章就把我这套 Agent 的搭建过程完整写一遍。不是那种"Hello World 级别的 demo",是真正能跑在生产环境里、支持多轮工具调用和错误恢复的方案。

先说结论

维度说明
模型选择Claude Opus 4.7(复杂推理)/ Claude Sonnet 4.6(日常任务,便宜一半多)
Agent 框架不用 LangChain,纯 Python 手撸 Agent Loop,可控性更强
工具调用Anthropic 原生 tool_use 协议,比 OpenAI 的 function calling 在多步推理上更稳
错误恢复工具执行失败后把报错原文喂回模型,让它自己决定重试还是换方案
实测效果5 个工具、3 轮平均调用深度,P95 延迟约 4.2s(含 extended thinking)

架构长什么样

整个 Agent 的核心就是一个循环:用户说话 → 模型思考要不要调工具 → 调工具 → 把结果喂回去 → 模型继续思考 → 直到它觉得可以给最终回复了。

graph TD
 A[用户输入] --> B[Claude Opus 4.7]
 B -->|stop_reason: tool_use| C{工具路由}
 C --> D[查天气 API]
 C --> E[查数据库]
 C --> F[发邮件]
 D --> G[工具结果]
 E --> G
 F --> G
 G --> B
 B -->|stop_reason: end_turn| H[最终回复给用户]

这个循环看着简单,但魔鬼在细节里。

环境准备

Python 3.11+,装两个包就够了:

pip install anthropic httpx

我没用 LangChain。之前用过一次,光是调试 LangChain 内部的 callback chain 就花了大半天,最后发现它帮我做的事情我自己 200 行代码就能搞定。Agent 这种需要精细控制每一步的场景,手撸反而更快。

方案一:基础 Agent Loop(单工具调用)

先搞一个最小可用版本。定义一个天气查询工具,让 Claude 自己决定什么时候调它。

import anthropic
import json

# 用聚合平台统一管理多个模型的 Key,省得每家单独申请
client = anthropic.Anthropic(
 api_key="your-key",
 base_url="https://api.ofox.ai/v1"
)

# 定义工具
tools = [
 {
 "name": "get_weather",
 "description": "查询指定城市的当前天气,返回温度和天气状况",
 "input_schema": {
 "type": "object",
 "properties": {
 "city": {
 "type": "string",
 "description": "城市名称,如 Beijing、Tokyo"
 }
 },
 "required": ["city"]
 }
 }
]

# 模拟工具执行
def execute_tool(name: str, params: dict) -> str:
 if name == "get_weather":
 # 实际项目这里调真实天气 API
 fake_data = {"Beijing": "28°C 晴", "Tokyo": "22°C 多云"}
 city = params.get("city", "")
 result = fake_data.get(city, f"未找到 {city} 的天气数据")
 return json.dumps({"weather": result}, ensure_ascii=False)
 return json.dumps({"error": f"未知工具: {name}"})

def agent_loop(user_message: str):
 messages = [{"role": "user", "content": user_message}]
 
 while True:
 response = client.messages.create(
 model="claude-sonnet-4-6-20260401",
 max_tokens=4096,
 tools=tools,
 messages=messages
 )
 
 # 检查是否需要调用工具
 if response.stop_reason == "tool_use":
 # 把模型的完整回复(含 tool_use block)加到对话
 messages.append({"role": "assistant", "content": response.content})
 
 # 找到所有 tool_use block 并执行
 tool_results = []
 for block in response.content:
 if block.type == "tool_use":
 print(f"[Agent] 调用工具: {block.name}({block.input})")
 result = execute_tool(block.name, block.input)
 tool_results.append({
 "type": "tool_result",
 "tool_use_id": block.id,
 "content": result
 })
 
 messages.append({"role": "user", "content": tool_results})
 
 elif response.stop_reason == "end_turn":
 # 模型觉得可以回复了
 final_text = ""
 for block in response.content:
 if hasattr(block, "text"):
 final_text += block.text
 return final_text

# 测试
answer = agent_loop("东京今天天气怎么样?适合出门吗?")
print(answer)

跑起来的输出大概是这样的:

[Agent] 调用工具: get_weather({"city": "Tokyo"})
东京今天 22°C,多云。温度挺舒服的,适合出门,不过建议带把伞以防万一。

这个基础版已经能用了。但真实场景里问题多得多。

方案二:生产级 Agent(多工具 + 错误恢复 + 调用深度限制)

私活甲方那边的需求是 5 个工具混着用,用户经常问一些需要连续调 2-3 个工具才能回答的问题,比如"帮我查一下上海明天的天气,如果下雨就给我发个邮件提醒"。

这时候有几个坑必须处理:

坑 1:无限循环。 模型有时候会反复调同一个工具,我测试的时候遇到过一次它连续调了 11 次 get_weather,每次传的参数都一样。必须加调用深度限制。

坑 2:工具报错后模型不知道怎么办。 比如数据库查询超时,如果你只返回一个空结果,模型会编造数据。正确做法是把完整报错信息喂回去。

坑 3:并行工具调用。 Claude 有时候会在一个回复里塞两个 tool_use block,意思是"这俩我都要调"。你得同时执行再一起返回。

import anthropic
import json
import traceback

client = anthropic.Anthropic(
 api_key="your-key",
 base_url="https://api.ofox.ai/v1"
)

MAX_TOOL_ROUNDS = 8 # 最多 8 轮工具调用,防止无限循环

tools = [
 {
 "name": "get_weather",
 "description": "查询城市天气",
 "input_schema": {
 "type": "object",
 "properties": {
 "city": {"type": "string"},
 "date": {"type": "string", "description": "日期,格式 YYYY-MM-DD,默认今天"}
 },
 "required": ["city"]
 }
 },
 {
 "name": "send_email",
 "description": "发送邮件",
 "input_schema": {
 "type": "object",
 "properties": {
 "to": {"type": "string"},
 "subject": {"type": "string"},
 "body": {"type": "string"}
 },
 "required": ["to", "subject", "body"]
 }
 },
 {
 "name": "query_database",
 "description": "用 SQL 查询业务数据库,返回 JSON 结果",
 "input_schema": {
 "type": "object",
 "properties": {
 "sql": {"type": "string", "description": "SELECT 语句,禁止 DELETE/UPDATE"}
 },
 "required": ["sql"]
 }
 }
]

def execute_tool(name: str, params: dict) -> str:
 """执行工具,出错时返回完整报错而不是静默失败"""
 try:
 if name == "get_weather":
 # 模拟:明天上海有雨
 return json.dumps({"city": params["city"], "weather": "小雨", "temp": "19°C"})
 elif name == "send_email":
 # 模拟发邮件
 if "@" not in params.get("to", ""):
 raise ValueError(f"邮箱格式错误: {params['to']}")
 return json.dumps({"status": "sent", "message_id": "msg_abc123"})
 elif name == "query_database":
 sql = params.get("sql", "")
 if any(kw in sql.upper() for kw in ["DELETE", "UPDATE", "DROP"]):
 raise PermissionError(f"禁止执行写操作: {sql}")
 return json.dumps({"rows": [{"id": 1, "name": "测试数据"}]})
 else:
 return json.dumps({"error": f"未知工具: {name}"})
 except Exception as e:
 # 关键:把完整报错返回给模型,让它自己决定怎么处理
 return json.dumps({
 "error": str(e),
 "traceback": traceback.format_exc()[-500:] # 截断,别太长
 })

def agent_loop(user_message: str, system_prompt: str = None):
 messages = [{"role": "user", "content": user_message}]
 
 system = system_prompt or (
 "你是一个智能助手,可以调用工具完成任务。"
 "如果工具返回错误,分析错误原因并尝试修正参数重试,最多重试 2 次。"
 "如果无法完成,诚实告知用户原因。"
 )
 
 for round_num in range(MAX_TOOL_ROUNDS):
 response = client.messages.create(
 model="claude-sonnet-4-6-20260401",
 max_tokens=4096,
 system=system,
 tools=tools,
 messages=messages
 )
 
 if response.stop_reason == "end_turn":
 return "".join(b.text for b in response.content if hasattr(b, "text"))
 
 if response.stop_reason != "tool_use":
 # 意外的 stop_reason,比如 max_tokens 截断
 print(f"[警告] 意外停止: {response.stop_reason}")
 return "".join(b.text for b in response.content if hasattr(b, "text"))
 
 messages.append({"role": "assistant", "content": response.content})
 
 tool_results = []
 for block in response.content:
 if block.type == "tool_use":
 print(f"[Round {round_num+1}] {block.name}({json.dumps(block.input, ensure_ascii=False)})")
 result = execute_tool(block.name, block.input)
 tool_results.append({
 "type": "tool_result",
 "tool_use_id": block.id,
 "content": result
 })
 
 messages.append({"role": "user", "content": tool_results})
 
 return "抱歉,工具调用轮次超限,请简化你的问题重试。"

# 测试多步骤任务
result = agent_loop(
 "帮我查一下上海明天天气,如果有雨就给 test@example.com 发个提醒邮件"
)
print(result)

跑出来是这样的:

[Round 1] get_weather({"city": "上海", "date": "2026-04-26"})
[Round 2] send_email({"to": "test@example.com", "subject": "上海明日天气提醒", "body": "明天上海预计小雨,19°C,记得带伞。"})

两轮工具调用,模型自己判断了"有雨→需要发邮件"这个逻辑。没有写任何 if-else。

踩坑记录

1. tool_use_id 不匹配会 400

一开始我手动拼 tool_result 的时候把 tool_use_id 写错了,Claude API 直接返回:

anthropic.BadRequestError: 400 {"type":"error","error":{"type":"invalid_request_error","message":"tool_use_id not found in previous assistant message: toolu_xxx"}}

报错信息倒是挺清楚的。每个 tool_result 的 id 必须严格对应前一条 assistant message 里的 tool_use block id。

2. extended thinking 和 tool_use 一起用的坑

后来想给 Opus 4.7 开 extended thinking 提升复杂推理质量,结果发现一个问题:开了 thinking 之后,max_tokens 要设得够大,不然 thinking 占了大部分 token 预算,实际回复被截断。我设了 16384 才稳定。

这个行为我也不确定是 bug 还是设计如此,反正多给 token 预算就对了。

3. Sonnet 4.6 vs Opus 4.7 的选择

不是所有场景都需要 Opus。我测下来,工具调用链路在 2 步以内的话,Sonnet 4.6 的表现和 Opus 差不多,价格便宜太多了。只有那种"查数据库 → 分析结果 → 决定下一步查什么 → 再查 → 汇总"这种 3 步以上的链路,Opus 的成功率才明显高出来。

我现在的做法是先让 Sonnet 跑,如果第一轮工具调用后模型回复里出现"我不确定"之类的犹豫表述,自动 fallback 到 Opus 重跑。折腾了两天才把这个降级逻辑调顺。

4. 工具描述写得烂 = Agent 废了一半

这个真的是血泪教训。我一开始 query_database 的 description 只写了"查询数据库"四个字,模型根本不知道数据库里有什么表、什么字段,生成的 SQL 全是瞎猜。后来我在 description 里加了表结构摘要("可用表:users(id,name,email), orders(id,user_id,amount,created_at)"),准确率直接翻倍。

工具描述就是 Agent 的说明书,写得越具体,模型越不容易瞎搞。

小结

搭一个能用的 AI Agent 不需要什么复杂框架,核心就是一个 while 循环 + 工具定义 + 错误处理。Claude 的 tool_use 协议在多步推理上确实比较稳,特别是 Opus 4.7 开了 extended thinking 之后,复杂任务链的完成率能到 90% 以上。

我目前还没找到比"把报错原文喂回模型"更好的错误恢复方案,如果你有更优雅的做法欢迎评论区聊。代码已经在跑了,甲方那边反馈还行,就是偶尔 Opus 的响应慢了点——P95 大概 4.2 秒,不过对客服场景来说还能接受。