主题
不用框架,60 行写一个最小 Agent 循环
Agent 本体就是一个 while 循环
很多人第一反应是上框架。我一开始也是,装了一堆依赖,结果调试时连模型到底收到了什么 prompt 都看不清。
后来我把项目里的 agent 重写成了 60 行代码,没有依赖,一个 while 循环。跑了几个月,没出过框架解决不了的问题。
这篇文章用 Anthropic Messages API(模型名 claude-sonnet-4-6,2026-10-07 从官方文档核实)演示这个循环长什么样。
先定义工具
模型不执行任何东西,它只是输出一个结构化的调用请求。执行永远在你的代码里。
python
import anthropic, json
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "查询某城市当前天气",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
def get_weather(city: str) -> str:
return json.dumps({"city": city, "temp_c": 18}, ensure_ascii=False)description 不是注释,是给模型看的。我建议写得像人话,别堆关键词。模型选不选这个工具,主要看它。
循环本体
python
def run(query: str, max_turns: int = 10) -> str:
messages = [{"role": "user", "content": query}]
for _ in range(max_turns):
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=tools,
tool_choice={"type": "auto"},
messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
if resp.stop_reason != "tool_use":
return resp.content[0].text # 模型说完了
results = []
for block in resp.content:
if block.type == "tool_use":
output = get_weather(**block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})核心判断只有一个:stop_reason 是不是 tool_use。是,就执行工具、把结果按 tool_result 块回填,继续循环;不是,模型认为事情办完了,直接返回文本。
tool_choice 有三档:auto 让模型自己决定、any 强制必须调一个、tool 强制调指定那一个(这三档的说明 2026-10-07 核对过官方文档)。日常用 auto 就够。
为什么循环一定能停
三个出口,缺一个都是死循环:
stop_reason != "tool_use":模型自然收尾,正常退出max_turns:模型反复调工具停不下来时兜底。我上次调试一个搜索工具,模型连调了 14 轮还没停,就是这个兜底救的命- API 报错:自己加 try/except,报错后退出而不是让异常炸掉
另一个常被忽略的点是 token 账。工具定义本身占输入 token,Claude 各主流模型每请求固定加 346 tokens(官方文档给的数字,2026-10-07 核实)。工具不多无所谓,堆到几十个工具时这笔账很可观,Anthropic 后来专门出了 Tool Search Tool 来省这笔开销。
我踩过的三个坑
第一,tool_result 的 content 必须是字符串。我直接把 dict 传进去,API 报 400,找了半小时才发现要 json.dumps。
第二,工具报错不要吞。数据库查不到就返回 {"error": "city not found"} 这种结构化信息给模型,它会自己换姿势重试。返回空字符串,模型会当成"查到了但没内容",越走越偏。
第三,OpenAI 的 Chat Completions 接口结构不一样:回填用 role: "tool" 消息,靠 tool_call_id 对齐,强制调用用 tool_choice: "required"(2026-10-07 核实过 OpenAI 社区公告,2024 年 4 月上线)。两家的 SDK 我都写过,迁移时最容易把 Anthropic 的 tool_result 块原样搬过去,跑不通。
什么时候该上框架
工具超过十个、需要持久化会话状态、要接 MCP(Model Context Protocol)服务器的时候,框架省事。
反过来说,工具就三五个、流程一条道走到黑的场景,60 行代码更好调试。每一步 messages 里有什么,print 一下全在眼前。
我的建议是先手写一遍。不用框架不代表不用看框架,看过 LangChain 的源码再回来看这个循环,你会发现大多数"魔法"就是这个 for 循环套了层壳。