资讯详情

从零手搓AI Agent:基于Anthropic API的ReAct循环与工具调用实战

📅 2026/10/8 20:17:58 | 华诺云谱 👁 阅读
从零手搓AI Agent:基于Anthropic API的ReAct循环与工具调用实战
1. 为什么我要从零手搓一个 AI Agent先说结论如果你只会调 API 拼一个“你问我答”的聊天框那叫套壳不叫 Agent。真正的 AI Agent 得能自己拆任务、自己选工具、自己看结果决定下一步甚至自己发现走不通了换条路。我最初动手写这个东西是因为受够了市面上那些“智能助手”——问它今天天气它答得挺溜让它帮我查个文件、改段代码、再顺手发个通知它就开始装傻。从零构建的价值在于你能精确控制每一轮循环里发生了什么。Anthropic API 的 tool use 能力给了我们一个非常干净的接口模型不直接执行任何东西它只输出“我想调用某个工具参数是这些”然后由你的 Python 代码去真正执行再把结果喂回去。这个“模型决策、代码执行”的分工就是 Agent 的心脏。搞懂了这个循环你就能搭出任何形态的 Agent——自动发消息的、自动整理文件的、自动跑数据分析的底层骨架一模一样。这篇文章适合谁写过一点 Python、知道函数和字典怎么用、但没真正搭过 Agent 的人。我会把每一步为什么这么做讲透参数怎么定、坑在哪、我踩过什么全都摊开说。你照着抄能跑起来你理解了能改成自己的东西。2. 先把 Agent 的骨架想清楚再动手2.1 Agent 和普通聊天机器人的本质区别普通聊天机器人是“输入→输出”的一次性映射。你发一句话模型回一句话结束。Agent 不一样它是一个循环模型思考→决定行动→执行行动→观察结果→再思考直到任务完成或达到终止条件。这个循环在业界常被称为 ReAct 模式Reasoning Acting是目前最主流、也最容易理解的 Agent 架构。我用一个生活类比来解释。普通聊天机器人像一个只会动嘴的顾问你问他什么他答什么但他不会帮你干活。Agent 像一个带工具箱的助理你说“帮我把客厅收拾干净”他会先看看客厅什么状况观察决定先收玩具还是先扫地推理拿起扫帚扫两下行动看看扫干净没有观察结果没干净就再扫循环干净了就停终止。这个循环里最关键的三个要素是工具定义、循环控制、终止判断。工具定义告诉模型它有哪些能力可用循环控制决定最多转多少圈、每圈怎么组织消息终止判断决定什么时候停下来避免死循环烧钱。2.2 为什么选 Anthropic API 而不是别的市面上能做 tool use 的 API 不少我选 Anthropic 的原因很实际它的 tool use 返回格式非常结构化模型会明确告诉你“我要调哪个工具、参数是什么”解析起来不费劲。而且它支持多轮工具调用模型可以在一次回复里要求调多个工具也可以根据上一轮工具的结果决定下一步。这对构建复杂 Agent 至关重要。另一个原因是它的消息格式清晰。你维护一个 messages 列表用户消息、模型回复、工具结果都往里塞模型自己会根据上下文判断该干嘛。你不需要手动拼接复杂的 prompt 模板省了很多心智负担。提示如果你之前只用过那种“一问一答”的接口第一次接触 tool use 会觉得消息列表的管理有点绕。别急后面我会把每一轮消息怎么加讲清楚。2.3 整体架构设计三层分离我把 Agent 拆成三层这个分层是我试过几种方案后觉得最清爽的决策层Anthropic API 负责。它接收对话历史和工具定义输出“下一步做什么”。执行层Python 函数负责。每个工具就是一个普通函数接收参数、执行操作、返回结果。调度层主循环负责。它把决策层的输出翻译成执行层的调用再把执行结果翻译回决策层能理解的消息格式。这三层分离的好处是你想加一个新工具只需要写一个 Python 函数 在工具定义列表里加一条描述主循环完全不用改。想换模型或换 API只动决策层。想改执行逻辑只动执行层。这种解耦在后期扩展时能救命。3. 环境准备与依赖安装的实操细节3.1 Python 版本选择和虚拟环境我强烈建议用 Python 3.10 或以上。原因很简单Anthropic 的官方 SDK 用到了较新的类型注解语法3.9 以下会报一些莫名其妙的错。我自己在 3.8 上折腾了半小时才发现是版本问题换 3.11 后一次跑通。虚拟环境必须建。不是可选项。你系统里可能装了一堆乱七八糟的包版本冲突会让你怀疑人生。用 venv 就行python -m venv agent-env source agent-env/bin/activate # Linux/Mac # agent-env\Scripts\activate # Windows建好之后你的 pip install 都装在这个隔离环境里不会污染全局。3.2 安装核心依赖只需要两个包pip install anthropic python-dotenvanthropic 是官方 SDKpython-dotenv 用来管理 API key避免把密钥硬编码在代码里。就这两个不需要 LangChain、不需要任何框架。从零构建的意义就在这你完全知道每一行在干嘛。注意不要把 API key 直接写在 .py 文件里然后传到代码仓库。用 .env 文件存并且把 .env 加进 .gitignore。我见过太多人因为这事被刷爆额度。3.3 验证环境是否就绪装完之后跑一行验证import anthropic print(anthropic.__version__)能打印出版本号就说明 SDK 装好了。如果报 ModuleNotFoundError八成是虚拟环境没激活或者 pip 装到了别的 Python 版本下。用which python和which pip确认一下路径是否一致。4. 核心循环的实现Agent 的心脏4.1 工具定义的数据结构工具定义本质上就是一段 JSON schema告诉模型“有这么个工具它叫什么、干什么用、需要什么参数”。Anthropic API 要求的格式是这样的tools [ { name: get_weather, description: 查询指定城市的当前天气。当用户询问天气相关问题时使用。, input_schema: { type: object, properties: { city: { type: string, description: 城市名称例如 北京 或 Shanghai } }, required: [city] } } ]这里有个经验之谈description 写得好不好直接决定模型会不会在正确的时机调用正确的工具。我一开始把 description 写成“查询天气”结果模型经常在该调工具的时候不调或者在不该调的时候乱调。后来改成“当用户询问天气相关问题时使用”命中率明显提升。description 要写清楚“什么时候用”而不只是“这是什么”。4.2 工具函数的实现每个工具对应一个 Python 函数。函数名和工具名对应参数和 schema 对应def get_weather(city: str) - str: # 实际项目中这里会调真实天气 API # 演示用假数据 fake_data { 北京: 晴25°C湿度 40%, 上海: 多云28°C湿度 65%, } return fake_data.get(city, f未找到 {city} 的天气数据)函数返回值必须是字符串或能被序列化的东西。我建议统一返回字符串省得后面处理各种类型。如果工具有复杂返回值在函数内部先 json.dumps 成字符串再返回。4.3 主循环的完整实现这是整个 Agent 最核心的部分我把它拆开讲import json from anthropic import Anthropic client Anthropic() def run_agent(user_message: str, max_turns: int 10): messages [{role: user, content: user_message}] for turn in range(max_turns): response client.messages.create( modelclaude-sonnet-4-20250514, max_tokens4096, toolstools, messagesmessages ) # 把模型回复加入历史 messages.append({role: assistant, content: response.content}) # 如果模型不再调用工具说明它认为任务完成 if response.stop_reason end_turn: # 提取最终文本回复 for block in response.content: if hasattr(block, text): return block.text return 任务完成但模型未返回文本。 # 处理工具调用 if response.stop_reason tool_use: tool_results [] for block in response.content: if block.type tool_use: 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 达到最大轮次限制任务未完成。这段代码里有几个关键点必须理解第一stop_reason 是循环的指挥棒。当它是end_turn说明模型觉得没什么可做的了直接给答案。当它是tool_use说明模型要求执行工具。你只需要根据这个字段决定下一步逻辑非常清晰。第二tool_use_id 必须原样回传。模型可能一次要求调多个工具每个工具调用有唯一的 id。你返回结果时必须带上对应的 id否则模型不知道哪个结果对应哪个调用。我一开始漏了这个字段模型直接报错。第三消息角色是交替的。用户消息、模型回复、工具结果在 messages 列表里交替出现。工具结果的角色是user不是tool这点和某些其他 API 不一样容易搞混。4.4 工具执行的分发逻辑execute_tool 函数负责把工具名映射到实际函数def execute_tool(name: str, inputs: dict) - str: if name get_weather: return get_weather(**inputs) # 加新工具就在这里加分支 return f未知工具{name}这个分发逻辑虽然简单但它是 Agent 扩展性的关键。每加一个工具就在这里加一个分支。工具多了之后可以改成字典映射但初期用 if-else 完全够用可读性还好。5. 让 Agent 真正能干活工具设计与避坑5.1 工具粒度的把握工具设计最难的决策是粒度。太粗模型不知道怎么用太细模型要调很多次才能完成一件事又慢又费 token。我的经验法则是一个工具对应一个原子操作但这个原子操作要有实际意义。比如“读文件”是一个好工具“打开文件句柄”就太细了“帮我重构整个项目”又太粗。判断标准是这个操作能不能用一句话描述清楚且不需要模型在中间做额外决策。举个例子如果你要做一个能自动发消息的 Agent工具应该是“发送消息到指定频道”而不是“打开输入框”“输入文字”“点击发送”三个工具。后者会让模型陷入操作细节前者让它专注在“发什么、发给谁”的决策上。5.2 参数设计的常见陷阱参数类型尽量用基础类型string、number、boolean。避免用嵌套对象因为模型在生成嵌套结构时容易出错。如果确实需要复杂参数拆成多个平铺参数。必填参数和可选参数要分清。required 列表里只放真正必须的。可选参数在 description 里说明默认行为。我踩过的坑是把一个可选参数标成了必填结果模型每次都要瞎编一个值填进去导致工具执行结果不对。提示参数名用英文description 可以用中文。模型对参数名的理解主要靠 description名字只要保持一致就行。5.3 工具返回结果的处理工具返回的内容会原样进入模型的上下文。这意味着返回结果的质量直接影响模型的下一步判断。几个原则返回结构化信息与其返回一大段自然语言不如返回 JSON 字符串。模型解析 JSON 比解析散文靠谱得多。控制返回长度如果一个工具返回几千字会迅速吃掉上下文窗口。在工具函数内部做截断或摘要。错误也要返回工具执行失败时不要抛异常让程序崩溃而是返回一个描述错误的字符串。模型看到错误信息后可能会换个方式重试这比直接崩掉优雅得多。def read_file(path: str) - str: try: with open(path, r, encodingutf-8) as f: content f.read() if len(content) 2000: return content[:2000] \n...[内容已截断] return content except FileNotFoundError: return f错误文件 {path} 不存在。 except Exception as e: return f错误读取文件失败原因{str(e)}这种“永不抛异常永远返回字符串”的风格是我写 Agent 工具函数时坚持的原则。6. 调试与排查那些文档不会告诉你的事6.1 模型不调工具怎么办这是最常见的问题。你定义了一个工具用户的问题明明该触发它但模型就是不用直接凭记忆回答。原因通常有三个description 不够明确。回到 4.1 节说的description 要写清楚“什么时候用”。加上触发场景的描述比如“当用户询问实时信息时使用”“当需要读取本地文件时使用”。系统提示没配合。在 system prompt 里明确告诉模型“你有工具可用遇到需要外部信息的任务时优先使用工具”。有时候模型需要一点推动。工具名太抽象。把工具名起得直白一点。search不如search_webprocess不如resize_image。名字本身就是给模型的提示。6.2 死循环怎么破Agent 陷入死循环通常表现为反复调同一个工具、反复得到同样的结果、永远不结束。防御手段有三层第一层是 max_turns 硬限制。我在主循环里设了默认 10 轮超过就强制退出。这个数字根据任务复杂度调整简单任务 5 轮够复杂任务可以到 20 轮。但绝不能不限。第二层是重复检测。在循环里记录每次工具调用的 (name, inputs) 组合如果连续两次完全相同就注入一条提示消息告诉模型“你刚才已经调过这个工具且结果相同请换一种方式或给出最终答案”。第三层是超时控制。如果单个工具执行时间过长加超时。Python 的 signal 模块或者 concurrent.futures 都能做看你的运行环境。6.3 常见问题速查表问题现象可能原因解决方法模型不调用工具description 不清晰补充触发场景描述报错 tool_use_id 缺失返回结果时漏了 id检查 tool_result 结构上下文超限工具返回内容太长在工具函数内截断循环不终止模型反复调同一工具加 max_turns 和重复检测参数解析失败schema 类型不匹配检查 input_schema 定义模型回复为空stop_reason 判断有误确认 end_turn 分支逻辑这张表是我实际调试中积累的基本上覆盖了 90% 的常见问题。遇到新问题先对照这张表排查能省很多时间。7. 从能跑到好用几个提升 Agent 质量的技巧7.1 系统提示的写法system prompt 是 Agent 的“人格设定”和“行为准则”。我一般会写三部分角色定义、能力说明、行为约束。角色定义告诉模型它是谁能力说明列出它有哪些工具、分别能干什么行为约束规定它不该干什么比如“不要编造工具返回结果之外的信息”“如果工具执行失败如实告知用户”。一个我常用的模板你是一个任务执行助手。你可以使用以下工具来完成任务 - get_weather查询城市天气 - read_file读取本地文件 规则 1. 需要外部信息时优先使用工具不要凭记忆回答。 2. 工具返回错误时尝试其他方式或如实告知用户。 3. 完成任务后给出简洁的总结。7.2 多工具协作的场景当 Agent 有多个工具时模型可以自己决定调用顺序。比如用户说“查一下北京天气然后写到 weather.txt 里”模型会先调 get_weather拿到结果后再调 write_file。这个编排能力是 Agent 相比固定流程脚本的最大优势。但要注意工具之间的数据传递靠的是模型理解不是代码硬编码。所以工具的输入输出格式要尽量一致和可预测。如果 get_weather 返回 JSONwrite_file 接收纯文本模型需要做一次转换这增加了出错概率。统一用字符串或统一用 JSON能减少这类问题。7.3 成本控制的实操经验Agent 的 token 消耗比普通对话高得多因为每一轮都要把完整历史发回去。控制成本的手段工具返回结果尽量精简别把整个文件内容塞回去。max_turns 别设太大够用就行。简单任务用便宜模型复杂任务再换强模型。可以在代码里根据任务类型动态选模型。定期清理不再需要的对话历史。如果任务已经完成没必要把之前的工具调用记录一直带着。我实测下来一个设计良好的 Agent 完成中等复杂度任务token 消耗在几千到一万之间。如果发现消耗异常高八成是工具返回内容太长或者循环次数过多。8. 部署与扩展的几点思考8.1 从脚本到服务本地跑通之后下一步通常是把它变成一个可调用的服务。最简单的做法是用 FastAPI 包一层from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class AgentRequest(BaseModel): message: str app.post(/agent) def handle(req: AgentRequest): result run_agent(req.message) return {result: result}这样任何能发 HTTP 请求的地方都能调用你的 Agent。注意并发问题如果多个请求同时进来每个请求要有独立的 messages 列表不能共享全局状态。把 run_agent 设计成无状态的纯函数每次调用创建新的 messages是最稳妥的做法。8.2 加新工具的标准流程当你需要 Agent 具备新能力时按这个流程走写一个 Python 函数接收基础类型参数返回字符串内部处理所有异常。在 tools 列表里加一条定义description 写清楚使用场景。在 execute_tool 里加一个分支。测试构造一个应该触发该工具的用户输入看模型是否正确调用。整个过程不需要改主循环这就是三层分离架构的威力。8.3 后续可以探索的方向这套骨架搭好之后可以往上叠很多东西。比如加一个“记忆”工具让 Agent 能把重要信息存到数据库下次对话时读出来。比如加一个“规划”步骤让模型先输出任务分解再逐步执行。比如加人工确认环节在敏感操作前暂停等用户确认后再继续。但所有这些扩展都建立在同一个核心循环之上。把第 4 节那个循环吃透剩下的都是在这个骨架上加肉。我个人的体会是Agent 的复杂度不在于代码量而在于你对“模型该做什么、代码该做什么”这条边界的理解。边界划清楚了代码自然就清晰了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑