007 讲清了 Agent 的灵魂——LLM 在「思考→行动→观察」的循环里,自己决定用什么工具。但当时它还是一张图、一段轨迹。今天我们把这张图焊成代码:定义几个工具,写一个循环,让模型真的能调用它们。你会惊讶地发现,一个能「动手做事」的 Agent,核心不过二三十行——魔法全在那个循环里。
01回顾:Agent 的三个零件(这次全上代码)
把 007 的四零件收敛成这次要写的三块:
- 🧠 大脑
:Claude,负责「决定下一步调用哪个工具、参数填什么」。 - 🔧 工具
:一份「工具说明书」(JSON Schema) + 真正执行的函数。 - 🔁 循环
:手写的 ReAct loop——调用 → 看要不要工具 → 执行 → 回填结果 → 再调用,直到收工。
🔑 最关键的一点(重申 007)
模型自己不执行任何代码。它只会「动嘴」——输出「我想调用 get_weather,参数 city=北京」。真正跑函数、拿结果、喂回去的,是我们下面要写的循环代码。模型负责决策,你的代码负责执行。
02要造什么(先看轨迹)
一个小助手:给它两个工具(查天气、计算器),问它「北京现在几度?换算成华氏度」,它会自己分两步调用工具,再给出答案:
agent.py — 运行轨迹
$python agent.py>北京现在几度?帮我换算成华氏度⚡ get_weather({'city': '北京'}) → {"city":"北京","temp_c":28}⚡ calculator({'expr': '28*9/5+32'}) → 82.4💬 北京现在约 28℃,换算成华氏度是 82.4°F。
注意:"先查天气、再算数"这个顺序没人写死——是模型在循环里自己临场决定的。这正是 Agent 区别于写死脚本的地方。
03第一步:给工具写「说明书」
模型靠工具的 name / description / input_schema 来判断「什么时候、怎么用」它。描述里写清「何时该调用」尤其重要:
tools = [ { "name": "get_weather", "description": "查询某城市的实时天气(摄氏度)。用户问天气时调用。", "input_schema": { "type": "object", "properties": {"city": {"type": "string", "description": "城市名,如 北京"}}, "required": ["city"], }, }, { "name": "calculator", "description": "计算一个数学表达式。需要精确算数时调用。", "input_schema": { "type": "object", "properties": {"expr": {"type": "string", "description": "如 28*9/5+32"}}, "required": ["expr"], }, }, ]
04第二步:写真正执行工具的函数
这是「手脚」——模型请求哪个工具,就在这里真跑:
defrun_tool(name, args): if name == "get_weather": fake = {"北京": 28, "上海": 31} # demo:真实项目请调天气 APIreturnf'{{"city":"{args["city"]}","temp_c":{fake.get(args["city"], 25)}}}'if name == "calculator": returnstr(eval(args["expr"])) # demo:真实项目别直接 eval(见"安全")return"unknown tool"
05第三步:Agent 循环(ReAct 的代码化)⭐
核心来了。把 007 的「思考→行动→观察」循环,翻译成这段代码——看它如何精确对应:
import anthropic client = anthropic.Anthropic()defagent(user_input, max_turns=6): messages = [{"role": "user", "content": user_input}] for _ inrange(max_turns): # 上限:防死循环 resp = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages, # 💭 思考:模型决定要不要用工具 ) if resp.stop_reason != "tool_use": # 不再要工具 → 收工,返回答案returnnext(b.text for b in resp.content if b.type == "text") # 把助手这一轮(含 tool_use 请求)加进对话历史 messages.append({"role": "assistant", "content": resp.content}) results = [] for blk in resp.content: if blk.type == "tool_use": # ⚡ 行动:执行模型点名的工具 out = run_tool(blk.name, blk.input) print(f" ⚡ {blk.name}({blk.input}) → {out}") results.append({ "type": "tool_result", "tool_use_id": blk.id, # 必须对应上 tool_use 的 id"content": out, }) # 👁 观察:把工具结果喂回去,进入下一轮 messages.append({"role": "user", "content": results}) return"(达到最大轮数仍未完成)"

看出来了吗?stop_reason == "tool_use" 就是模型在说「我需要工具」;我们执行后用 tool_result 回填,再循环——这就是 ReAct。循环里没有一句业务逻辑是写死的,全是模型临场决策。
06完整脚本(agent.py)
# agent.py —— 一个会调用工具的最小 Agent# 依赖:pip install anthropic ;设置 ANTHROPIC_API_KEYimport anthropic client = anthropic.Anthropic() tools = [ {"name": "get_weather", "description": "查询城市实时天气(摄氏度)。用户问天气时调用。", "input_schema": {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}, {"name": "calculator", "description": "计算数学表达式。需要精确算数时调用。", "input_schema": {"type":"object","properties":{"expr":{"type":"string"}},"required":["expr"]}}, ]defrun_tool(name, args): if name == "get_weather": fake = {"北京":28, "上海":31} returnf'{{"city":"{args["city"]}","temp_c":{fake.get(args["city"],25)}}}'if name == "calculator": returnstr(eval(args["expr"])) # demo onlyreturn"unknown tool"defagent(user_input, max_turns=6): messages = [{"role":"user","content":user_input}] for _ inrange(max_turns): resp = client.messages.create(model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages) if resp.stop_reason != "tool_use": returnnext(b.text for b in resp.content if b.type == "text") messages.append({"role":"assistant","content":resp.content}) results = [] for blk in resp.content: if blk.type == "tool_use": out = run_tool(blk.name, blk.input) print(f" ⚡ {blk.name}({blk.input}) → {out}") results.append({"type":"tool_result","tool_use_id":blk.id,"content":out}) messages.append({"role":"user","content":results}) return"(达到最大轮数)"if __name__ == "__main__": while (q := input("\n> ").strip()) != "q": print("💬", agent(q))
三十来行,一个能自主调用工具的 Agent 就跑起来了。把 tools 换成搜索、数据库、发邮件、操作文件——它就能干越来越复杂的活。
07更省心:SDK 自带的 tool_runner
手写循环有助于理解原理;真正干活时,官方 SDK 的 tool_runner 能把那个循环替你跑掉。用装饰器把普通函数变成工具,schema 自动生成:
from anthropic import beta_tool@beta_tooldefget_weather(city: str) -> str: """查询某城市的实时天气(摄氏度)。"""# 文档字符串 = descriptionreturnf'{{"city":"{city}","temp_c":28}}'runner = client.beta.messages.tool_runner( # SDK 自动跑思考-行动-观察循环 model="claude-opus-4-8", max_tokens=1024, tools=[get_weather], messages=[{"role":"user","content":"北京天气怎么样?"}], )for message in runner: # 循环自动进行,直到模型不再要工具print(message)
| 全掌控 | ||
08安全 & 常见坑
⚠️ 工具执行是「你的代码」,责任在你
① 危险动作要设围栏:能删文件、发邮件、花钱的工具,务必做输入校验、加「人在环中」确认(human-in-the-loop)。demo 里的 eval() 千万别上生产——用安全的表达式解析器替代。
② 永远设循环上限:模型可能反复调工具或陷入死循环,max_turns 是必备的保险丝。
③ 工具描述决定成败:写清「何时该用」比「是什么」更重要——描述含糊,模型要么该用不用、要么乱用(呼应 005)。
④ tool_result 的 id 必须对上:每个结果的 tool_use_id 要精确匹配对应的 tool_use 块,否则报错。
⑤ 错误也要回传:工具失败时返回带 "is_error": true 的 tool_result,让模型知道并换个思路,而不是直接崩。
09今日小测验
Q1 代码里,模型是怎么"告诉"我们它想调用工具的?我们又怎么把结果给回去?
答:响应的 stop_reason == "tool_use" 且 content 里有 tool_use 块(含 name/input/id)表示它要调工具。我们执行后,用一条 user 消息回填 tool_result 块(tool_use_id 对应上),再继续循环。
Q2 "先查天气再算数"的顺序是谁决定的?这说明 Agent 和写死脚本的本质区别是什么?
答:是模型在循环里每一轮临场决定的,没有人写死。区别在于:Agent 能应对没预设过的情况、自主规划步骤,而脚本只能按固定流程走。
Q3 为什么一定要 max_turns 上限?
答:模型可能反复调用工具、绕圈子甚至陷入死循环,每轮都烧 token。上限是"保险丝",保证程序一定会终止,避免无限循环和失控成本。
Q4 手写循环和 SDK 的 tool_runner,各适合什么场景?
答:手写循环适合学原理、以及需要精细控制的场景(自定义日志、人工审批、条件执行);tool_runner 把循环代管了,适合快速开发标准场景。原理一致,只是谁来驱动循环的差别。
10小结 & 下一站
🎯 带走这一句
一个会用工具的 Agent = 工具说明书(JSON) + 执行函数 + 一个 ReAct 循环。循环里:模型 tool_use → 你执行 → 回填 tool_result → 再问,直到 end_turn。模型只做决策、代码负责执行,安全与上限由你兜底。007 的那张图,就这么活了。
🧪 动手作业:给它加第三个工具(比如「搜索」或「查数据库」),看它会不会在合适的问题上自己用起来。 📄 官方工具使用文档:Anthropic Tool Use 🏗️ 进阶阅读:Building Effective Agents ⏭️ 下一期预告(第 012 期):给 Agent 接上「真工具」——把上一季的 RAG 变成 Agent 的一个工具,让它自己决定何时查知识库。
夜雨聆风